diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworkProgress.tsx deleted file mode 100644 index c9c3b3cee6..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworkProgress.tsx +++ /dev/null @@ -1,184 +0,0 @@ -"use client"; - -import { useComplianceScores } from "@/hooks/use-compliance-scores"; -import { useI18n } from "@/locales/client"; -import type { - Framework, - OrganizationControl, - OrganizationFramework, -} from "@bubba/db/types"; -import { Button } from "@bubba/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; -import { FileStack } from "lucide-react"; -import Link from "next/link"; -import { useMediaQuery } from "@bubba/ui/hooks"; -import type { ReactNode } from "react"; -import { useParams } from "next/navigation"; -import { auth } from "@/auth"; -interface Props { - frameworks: (OrganizationFramework & { - organizationControl: OrganizationControl[]; - framework: Framework; - })[]; -} - -export function FrameworkProgress({ frameworks }: Props) { - const t = useI18n(); - const { - policiesCompliance, - evidenceTasksCompliance, - cloudTestsCompliance, - overallCompliance, - isLoading, - } = useComplianceScores({ frameworks }); - - const { orgId } = useParams<{ orgId: string }>(); - - const isMobile = useMediaQuery("(max-width: 640px)"); - - const CircleProgress = ({ - percentage, - label, - href, - }: { - percentage: number; - label: ReactNode; - href: string; - }) => ( - -
- - {/* Background circle */} - - {/* Progress circle */} - - -
-
{percentage}%
-
-
-
- {label} -
- - ); - - return ( - - - {t("frameworks.overview.progress.title")} - - - {isLoading ? ( -
-
-

- Loading compliance data... -

-
- ) : frameworks.length === 0 ? ( -
- -

- {t("frameworks.overview.progress.empty.title")} -

-

- {t("frameworks.overview.progress.empty.description")} -

- -
- ) : ( -
- {/* Main compliance circle */} -
-
- - {/* Background circle */} - - {/* Progress circle */} - - -
-
- {overallCompliance}% -
-
Compliant
-
-
-
- - {/* Three smaller circles */} -
- - - Evidence - Evidence Tasks - - } - href={`/${orgId}/evidence/list`} - /> - - Tests - Cloud Tests - - } - href={`/${orgId}/tests`} - /> -
-
- )} - - - ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/RequirementStatusChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/RequirementStatusChart.tsx deleted file mode 100644 index 13345995f1..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/RequirementStatusChart.tsx +++ /dev/null @@ -1,177 +0,0 @@ -"use client"; - -import { useI18n } from "@/locales/client"; -import type { Framework, OrganizationFramework } from "@bubba/db/types"; -import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; -import { Progress } from "@bubba/ui/progress"; -import Link from "next/link"; -import { useMemo } from "react"; -import type { OrganizationControlType } from "../overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns"; -import { useOrganizationCategories } from "../overview/frameworks/[frameworkId]/hooks/useOrganizationCategories"; -import { useParams } from "next/navigation"; - -interface Props { - frameworks: (OrganizationFramework & { - framework: Framework; - })[]; -} - -// Helper function to check if a control is compliant based on its requirements -const isControlCompliant = (control: OrganizationControlType) => { - // First, check if the control has the direct status of "compliant" - if (control.status === "compliant") { - return true; - } - - // Then check the requirements if they exist - const requirements = control.requirements; - - if (!requirements || requirements.length === 0) { - return false; - } - - const totalRequirements = requirements.length; - const completedRequirements = requirements.filter((req) => { - let isCompleted = false; - - switch (req.type) { - case "policy": - isCompleted = req.organizationPolicy?.status === "published"; - break; - case "file": - isCompleted = !!req.fileUrl; - break; - case "evidence": - isCompleted = req.organizationEvidence?.published === true; - break; - default: - isCompleted = req.published || false; - } - - return isCompleted; - }).length; - - return completedRequirements === totalRequirements; -}; - -// Individual FrameworkCard component -function FrameworkCard({ - framework, -}: { framework: OrganizationFramework & { framework: Framework } }) { - const { data: organizationCategories, isLoading } = useOrganizationCategories( - framework.framework.id, - ); - const { orgId } = useParams<{ orgId: string }>(); - - // Transform the organizationCategories into controls - const controls = useMemo(() => { - if (!organizationCategories) return []; - - return organizationCategories.flatMap((category) => - category.organizationControl.map((control) => ({ - code: control.control.code, - description: control.control.description, - name: control.control.name, - status: control.status, - id: control.id, - frameworkId: framework.framework.id, - category: category.name, - requirements: control.OrganizationControlRequirement, - })), - ); - }, [ - organizationCategories, - framework.framework.id, - ]) as OrganizationControlType[]; - - // Calculate framework compliance based on controls - const compliance = useMemo(() => { - if (isLoading || controls.length === 0) return 0; - - const totalControls = controls.length; - const compliantControls = controls.filter(isControlCompliant).length; - - return totalControls > 0 - ? Math.round((compliantControls / totalControls) * 100) - : 0; - }, [controls, isLoading]); - - if (isLoading) { - return ( -
-
-
-
-
-
-
- ); - } - - return ( - -
-
- {framework.framework.name.substring(0, 2).toUpperCase()} -
-
-
-
-

{framework.framework.name}

- - {compliance}% Compliant - -
- -
- - ); -} - -// Main component -export function RequirementStatus({ frameworks }: Props) { - const t = useI18n(); - const isLoading = !frameworks; - - return ( - - - {t("frameworks.title")} - - - {isLoading ? ( -
-
-

- Loading compliance data... -

-
- ) : frameworks.length === 0 ? ( -
-

- {t("frameworks.overview.empty.description")} -

-
- ) : ( -
- {/* Framework List */} -
- {frameworks.map((framework) => ( - - ))} -
-
- )} - - - ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/layout.tsx deleted file mode 100644 index a1d11fa0f9..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { auth } from "@/auth"; -import { getI18n } from "@/locales/server"; -import { redirect } from "next/navigation"; -import { SecondaryMenu } from "@bubba/ui/secondary-menu"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const t = await getI18n(); - const session = await auth(); - - if (!session?.user?.organizationId) { - redirect("/auth"); - } - - return ( -
- - -
{children}
-
- ); -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationCategories.ts deleted file mode 100644 index bd71206a4a..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationCategories.ts +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import useSWR from "swr"; -import { getOrganizationCategories } from "../actions/getOrganizationCategories"; - -async function fetchOrganizationCategories(frameworkId: string) { - const result = await getOrganizationCategories({ frameworkId }); - - if (!result) { - throw new Error("Failed to fetch frameworks"); - } - - const data = result.data?.data; - if (!data) { - throw new Error("Invalid response from server"); - } - - return data; -} - -export function useOrganizationCategories(frameworkId: string) { - const { data, error, isLoading, mutate } = useSWR( - ["organization-categories", frameworkId], - () => fetchOrganizationCategories(frameworkId), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }, - ); - - return { - data, - isLoading, - error, - mutate, - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationFramework.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationFramework.ts deleted file mode 100644 index bc84d39d9f..0000000000 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/hooks/useOrganizationFramework.ts +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import useSWR from "swr"; -import { getOrganizationFramework } from "../actions/getOrganizationFramework"; - -async function fetchOrganizationFramework(frameworkId: string) { - const result = await getOrganizationFramework({ frameworkId }); - - if (!result) { - throw new Error("Failed to fetch frameworks"); - } - - const data = result.data?.data; - if (!data) { - throw new Error("Invalid response from server"); - } - - return data; -} - -export function useOrganizationFramework(frameworkId: string) { - const { data, error, isLoading, mutate } = useSWR( - ["organization-framework", frameworkId], - () => fetchOrganizationFramework(frameworkId), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - } - ); - - return { - data, - isLoading, - error, - mutate, - }; -} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx index 9a0e9b7e89..423d3d5928 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/layout.tsx @@ -5,40 +5,40 @@ import dynamic from "next/dynamic"; import { redirect } from "next/navigation"; const HotKeys = dynamic( - () => import("@/components/hot-keys").then((mod) => mod.HotKeys), - { - ssr: true, - }, + () => import("@/components/hot-keys").then((mod) => mod.HotKeys), + { + ssr: true, + }, ); export default async function Layout({ - children, - params, + children, + params, }: { - children: React.ReactNode; - params: Promise<{ orgId: string }>; + children: React.ReactNode; + params: Promise<{ orgId: string }>; }) { - const session = await auth(); - const orgId = (await params).orgId; + const session = await auth(); + const orgId = (await params).orgId; - if (!session) { - redirect("/auth"); - } + if (!session) { + redirect("/auth"); + } - if (!orgId) { - redirect("/"); - } + if (!orgId) { + redirect("/overview"); + } - return ( -
- + return ( +
+ -
-
-
{children}
-
+
+
+
{children}
+
- -
- ); + +
+ ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworkProgress.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworkProgress.tsx new file mode 100644 index 0000000000..3fc10f4723 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworkProgress.tsx @@ -0,0 +1,165 @@ +"use client"; + +import { useI18n } from "@/locales/client"; +import { Button } from "@bubba/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import { useMediaQuery } from "@bubba/ui/hooks"; +import { FileStack } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import type { ReactNode } from "react"; +import type { ComplianceScoresProps } from "./types"; +import type { + Framework, + OrganizationControl, + OrganizationFramework, +} from "@bubba/db/types"; + +interface FrameworkProgressProps { + frameworks: (OrganizationFramework & { + organizationControl: OrganizationControl[]; + framework: Framework; + })[]; + complianceScores: ComplianceScoresProps; +} + +export function FrameworkProgress({ + frameworks, + complianceScores, +}: FrameworkProgressProps) { + const t = useI18n(); + const { + policiesCompliance, + evidenceTasksCompliance, + cloudTestsCompliance, + overallCompliance, + } = complianceScores; + + const { orgId } = useParams<{ orgId: string }>(); + + const isMobile = useMediaQuery("(max-width: 640px)"); + + const CircleProgress = ({ + percentage, + label, + href, + }: { + percentage: number; + label: ReactNode; + href: string; + }) => ( + +
+ + {/* Background circle */} + + {/* Progress circle */} + + +
+
{percentage}%
+
+
+
+ {label} +
+ + ); + + if (!frameworks.length) return null; + + return ( + + + {t("frameworks.overview.progress.title")} + + +
+ {/* Main compliance circle */} +
+
+ + {/* Background circle */} + + {/* Progress circle */} + + +
+
+ {overallCompliance}% +
+
Compliant
+
+
+
+ + {/* Three smaller circles */} +
+ + + Evidence + Evidence Tasks + + } + href={`/${orgId}/evidence/list`} + /> + + Tests + Cloud Tests + + } + href={`/${orgId}/tests`} + /> +
+
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksGrid.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksGrid.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksGrid.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksGrid.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksOverview.tsx similarity index 50% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksOverview.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksOverview.tsx index 1831d88465..d42d1290fb 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/components/FrameworksOverview.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/FrameworksOverview.tsx @@ -5,22 +5,35 @@ import type { OrganizationControl, OrganizationFramework, } from "@bubba/db/types"; +import type { ComplianceScoresProps, FrameworkWithCompliance } from "./types"; import { FrameworkProgress } from "./FrameworkProgress"; import { RequirementStatus } from "./RequirementStatusChart"; -export const FrameworksOverview = ({ - frameworks, -}: { +export interface FrameworksOverviewProps { frameworks: (OrganizationFramework & { organizationControl: OrganizationControl[]; framework: Framework; })[]; -}) => { + complianceScores: ComplianceScoresProps; + frameworksWithCompliance: FrameworkWithCompliance[]; +} + +export const FrameworksOverview = ({ + frameworks, + complianceScores, + frameworksWithCompliance, +}: FrameworksOverviewProps) => { return (
- - + +
); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/RequirementStatusChart.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/RequirementStatusChart.tsx new file mode 100644 index 0000000000..f3eb93f710 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/RequirementStatusChart.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useI18n } from "@/locales/client"; +import type { Framework, OrganizationFramework } from "@bubba/db/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Progress } from "@bubba/ui/progress"; +import Link from "next/link"; +import { useParams } from "next/navigation"; + +interface FrameworkWithCompliance { + framework: OrganizationFramework & { + framework: Framework; + }; + compliance: number; +} + +interface Props { + frameworks: (OrganizationFramework & { + framework: Framework; + })[]; + frameworksWithCompliance: FrameworkWithCompliance[]; +} + +// Individual FrameworkCard component +function FrameworkCard({ + framework, + compliance, +}: { + framework: OrganizationFramework & { framework: Framework }; + compliance: number; +}) { + const { orgId } = useParams<{ orgId: string }>(); + + return ( + +
+
+ {framework.framework.name.substring(0, 2).toUpperCase()} +
+
+
+
+

{framework.framework.name}

+ + {compliance}% Compliant + +
+ +
+ + ); +} + +// Main component +export function RequirementStatus({ + frameworks, + frameworksWithCompliance, +}: Props) { + const t = useI18n(); + + if (!frameworks.length || !frameworksWithCompliance.length) return null; + + return ( + + + {t("frameworks.title")} + + +
+ {/* Framework List */} +
+ {frameworksWithCompliance.map(({ framework, compliance }) => ( + + ))} +
+
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/types.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/types.ts new file mode 100644 index 0000000000..7617132cfe --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/components/types.ts @@ -0,0 +1,29 @@ +import type { + Framework, + OrganizationEvidence, + OrganizationFramework, + OrganizationIntegrationResults, + OrganizationPolicy, +} from "@bubba/db/types"; + +export interface ComplianceScoresProps { + policiesCompliance: number; + evidenceTasksCompliance: number; + cloudTestsCompliance: number; + overallCompliance: number; + frameworkCompliance: { + id: string; + name: string; + compliance: number; + }[]; + policies: OrganizationPolicy[]; + evidenceTasks: OrganizationEvidence[]; + tests: OrganizationIntegrationResults[]; +} + +export interface FrameworkWithCompliance { + framework: OrganizationFramework & { + framework: Framework; + }; + compliance: number; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getComplianceScores.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getComplianceScores.ts new file mode 100644 index 0000000000..b9a0dbcfa2 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getComplianceScores.ts @@ -0,0 +1,267 @@ +"use server"; + +import { db } from "@bubba/db"; +import type { + Framework, + OrganizationControl, + OrganizationEvidence, + OrganizationIntegrationResults, + OrganizationFramework, + OrganizationPolicy, +} from "@bubba/db/types"; + +interface ComplianceScoresResult { + policiesCompliance: number; + evidenceTasksCompliance: number; + cloudTestsCompliance: number; + overallCompliance: number; + frameworkCompliance: { + id: string; + name: string; + compliance: number; + }[]; + policies: OrganizationPolicy[]; + evidenceTasks: OrganizationEvidence[]; + tests: any[]; +} + +export async function getComplianceScores( + organizationId: string, + frameworks: (OrganizationFramework & { + framework: Framework; + organizationControl: OrganizationControl[]; + })[] +): Promise { + // Get all policies for the organization + const policies = await db.organizationPolicy.findMany({ + where: { organizationId }, + include: { + policy: { + select: { + id: true, + name: true, + description: true, + slug: true, + }, + }, + }, + }); + + // Get all policies by framework + const policiesByFramework = await db.policyFramework.findMany({ + where: { + policy: { + OrganizationPolicy: { + some: { + organizationId, + }, + }, + }, + }, + include: { + policy: { + select: { + id: true, + OrganizationPolicy: { + where: { organizationId }, + select: { id: true, status: true }, + }, + }, + }, + }, + }); + + // Map policy framework data to a simpler structure + const mappedPoliciesByFramework = policiesByFramework.map((pf) => ({ + frameworkId: pf.frameworkId, + policyId: pf.policyId, + organizationPolicyId: pf.policy.OrganizationPolicy[0]?.id, + status: pf.policy.OrganizationPolicy[0]?.status, + })); + + // Get all evidence tasks for the organization + const evidenceTasks = await db.organizationEvidence.findMany({ + where: { organizationId }, + include: { + evidence: true, + }, + }); + + // Map evidence tasks to framework + const evidenceByFramework = evidenceTasks + .filter((task) => task.frameworkId) // Only include tasks with a frameworkId + .map((task) => ({ + id: task.id, + frameworkId: task.frameworkId, + evidenceId: task.id, + published: task.published, + })); + + // Get all tests for the organization (using organizationIntegrationResults as per the getTests action) + const integrationResults = await db.organizationIntegrationResults.findMany({ + where: { organizationId }, + include: { + organizationIntegration: { + select: { + id: true, + name: true, + integration_id: true, + }, + }, + }, + }); + + // Transform the data to match the expected format + const tests = integrationResults.map((result) => ({ + id: result.id, + severity: result.label, + result: result.status, + title: result.title || result.organizationIntegration.name, + provider: result.organizationIntegration.integration_id, + createdAt: result.completedAt || new Date(), + })); + + // Calculate policies compliance + const policiesCompliance = + policies.length > 0 + ? Math.round( + (policies.filter((p) => p.status === "published").length / + policies.length) * + 100 + ) + : 0; + + // Calculate evidence tasks compliance + const evidenceTasksCompliance = + evidenceTasks.length > 0 + ? Math.round( + (evidenceTasks.filter((task) => task.published === true).length / + evidenceTasks.length) * + 100 + ) + : 0; + + // Calculate cloud tests compliance (checking for "PASSED" status as per the type definition) + const cloudTestsCompliance = + tests.length > 0 + ? Math.round( + (tests.filter((test) => test.result?.toUpperCase() === "PASSED") + .length / + tests.length) * + 100 + ) + : 0; + + // Calculate framework-specific compliance + const frameworkCompliance = frameworks.map((framework) => { + // Calculate framework controls compliance + const totalControls = framework.organizationControl.length; + + // Count controls with any progress (in_progress or compliant) + const inProgressControls = framework.organizationControl.filter( + (control) => control.status === "in_progress" + ).length; + + const compliantControls = framework.organizationControl.filter( + (control) => control.status === "compliant" + ).length; + + // For compliance percentage, count both in_progress (partial) and compliant (full) + // Give in_progress controls half weight compared to compliant ones + const progressWeight = 0.5; // Weight for in_progress controls + const weightedProgress = + compliantControls + inProgressControls * progressWeight; + + // Calculate compliance percentage based on controls + const controlsCompliance = + totalControls > 0 + ? Math.round((weightedProgress / totalControls) * 100) + : 0; + + // Find policies for this specific framework + const frameworkPolicies = mappedPoliciesByFramework.filter( + (policy) => policy.frameworkId === framework.framework.id + ); + + // Calculate framework-specific policies compliance + const frameworkPoliciesCompliance = + frameworkPolicies.length > 0 + ? Math.round( + (frameworkPolicies.filter( + (frameworkPolicy) => frameworkPolicy.status === "published" + ).length / + frameworkPolicies.length) * + 100 + ) + : null; + + // Find evidence tasks related to this framework + const frameworkEvidenceTasks = evidenceByFramework.filter( + (evidence) => evidence.frameworkId === framework.framework.id + ); + + // Calculate framework-specific evidence tasks compliance + const frameworkEvidenceTasksCompliance = + frameworkEvidenceTasks.length > 0 + ? Math.round( + (frameworkEvidenceTasks.filter( + (evidence) => evidence.published === true + ).length / + frameworkEvidenceTasks.length) * + 100 + ) + : null; + + // Calculate overall framework compliance as average of all components with data + const complianceScores = [controlsCompliance]; + if (frameworkPoliciesCompliance !== null) + complianceScores.push(frameworkPoliciesCompliance); + if (frameworkEvidenceTasksCompliance !== null) + complianceScores.push(frameworkEvidenceTasksCompliance); + + const totalCompliance = + complianceScores.length > 0 + ? Math.round( + complianceScores.reduce((sum, score) => sum + score, 0) / + complianceScores.length + ) + : controlsCompliance; + + return { + id: framework.framework.id, + name: framework.framework.name, + compliance: totalCompliance, + }; + }); + + // Calculate overall compliance as the average of all available scores + const calculateOverallCompliance = () => { + // Count how many categories have data + const categoriesWithData = [ + policies.length > 0, + evidenceTasks.length > 0, + tests.length > 0, + ].filter(Boolean).length; + + // If no categories have data, return 0 + if (categoriesWithData === 0) return 0; + + // Calculate the sum of all compliance scores + const totalScore = + policiesCompliance + evidenceTasksCompliance + cloudTestsCompliance; + + // Return the average, rounded to the nearest integer + return Math.round(totalScore / categoriesWithData); + }; + + return { + policiesCompliance, + evidenceTasksCompliance, + cloudTestsCompliance, + overallCompliance: calculateOverallCompliance(), + frameworkCompliance, + policies, + evidenceTasks, + tests, + }; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworkCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworkCategories.ts new file mode 100644 index 0000000000..4efdfee793 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworkCategories.ts @@ -0,0 +1,127 @@ +"use server"; + +import { db } from "@bubba/db"; +import type { + ComplianceStatus, + Framework, + OrganizationControlRequirement, + OrganizationEvidence, + OrganizationFramework, + OrganizationPolicy, +} from "@bubba/db/types"; + +// Define a type for the control with its requirements as returned from the database +type ControlWithRequirements = { + id: string; + status: ComplianceStatus; + control: { + code: string; + description: string | null; + name: string; + }; + OrganizationControlRequirement: Array< + OrganizationControlRequirement & { + organizationPolicy: Pick | null; + organizationEvidence: Pick | null; + } + >; +}; + +// Helper function to check if a control is compliant based on its requirements +const isControlCompliant = (control: ControlWithRequirements) => { + // First, check if the control has the direct status of "compliant" + if (control.status === "compliant") { + return true; + } + + // Then check the requirements if they exist + const requirements = control.OrganizationControlRequirement; + + if (!requirements || requirements.length === 0) { + return false; + } + + const totalRequirements = requirements.length; + const completedRequirements = requirements.filter((req) => { + let isCompleted = false; + + switch (req.type) { + case "policy": + isCompleted = req.organizationPolicy?.status === "published"; + break; + case "file": + isCompleted = !!req.fileUrl; + break; + case "evidence": + isCompleted = req.organizationEvidence?.published === true; + break; + default: + isCompleted = req.published; + } + + return isCompleted; + }).length; + + return completedRequirements === totalRequirements; +}; + +export async function getFrameworkCategories( + organizationId: string, + frameworks: (OrganizationFramework & { framework: Framework })[] +) { + // For each framework, get the categories and controls + const frameworksWithCompliance = await Promise.all( + frameworks.map(async (framework) => { + const organizationCategories = await db.organizationCategory.findMany({ + where: { + organizationId, + frameworkId: framework.framework.id, + }, + include: { + organizationControl: { + include: { + control: true, + OrganizationControlRequirement: { + include: { + organizationPolicy: { + select: { + status: true, + }, + }, + organizationEvidence: { + select: { + published: true, + }, + }, + }, + }, + }, + }, + }, + }); + + // Transform the categories into a list of controls + const controls = organizationCategories.flatMap((category) => + category.organizationControl.map((control) => control) + ); + + // Calculate compliance percentage + const totalControls = controls.length; + const compliantControls = controls.filter((control) => + isControlCompliant(control as ControlWithRequirements) + ).length; + + const compliance = + totalControls > 0 + ? Math.round((compliantControls / totalControls) * 100) + : 0; + + return { + framework, + compliance, + }; + }) + ); + + return frameworksWithCompliance; +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworks.ts new file mode 100644 index 0000000000..a96e8f8b97 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/data/getFrameworks.ts @@ -0,0 +1,15 @@ +"use server"; + +import { db } from "@bubba/db"; + +export const getFrameworks = async (organizationId: string) => { + const frameworks = await db.organizationFramework.findMany({ + where: { organizationId: organizationId }, + include: { + organizationControl: true, + framework: true, + }, + }); + + return frameworks; +}; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationCategories.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationControlsProgress.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/actions/getOrganizationFramework.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/FrameworkControls.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/FrameworkOverview.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTable.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableColumns.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/components/table/FrameworkControlsTableHeader.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/data/getFramework.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFramework.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/data/getFramework.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/data/getFrameworkCategories.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/lib/utils.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/lib/utils.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/lib/utils.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/loading.tsx new file mode 100644 index 0000000000..2cee56ed75 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/loading.tsx @@ -0,0 +1,119 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Skeleton } from "@bubba/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@bubba/ui/table"; + +export default function Loading() { + return ( +
+ {/* Framework Overview Skeleton */} +
+ {/* Framework Info Card */} + + + + + + + + + + + + + + {/* Compliance Progress Card */} + + + + + + + +
+ + +
+
+
+ + {/* Assessment Status Card */} + + + + + + + +
+
+ + +
+
+ + +
+
+
+
+
+ + {/* Framework Controls Table Skeleton */} +
+ + + + {["code", "name", "status"].map((column) => ( + + + + ))} + + + + {[ + "control1", + "control2", + "control3", + "control4", + "control5", + "control6", + "control7", + "control8", + "control9", + "control10", + "control11", + "control12", + "control13", + "control14", + "control15", + "control16", + "control17", + "control18", + ].map((control) => ( + + {["code", "name", "status"].map((column) => ( + + + + ))} + + ))} + +
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/page.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/[frameworkId]/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/[frameworkId]/page.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControl.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControl.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControl.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/SingleControlSkeleton.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTable.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableColumns.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/ControlRequirementsTableHeader.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/types.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/types.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/components/table/types.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/components/table/types.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/data/getControl.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getControl.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/data/getControl.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/data/getOrganizationControlProgress.ts diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/loading.tsx new file mode 100644 index 0000000000..1d1decdb0e --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/loading.tsx @@ -0,0 +1,97 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Skeleton } from "@bubba/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@bubba/ui/table"; + +export default function Loading() { + return ( +
+
+ {/* Control Info Cards */} +
+ {/* Control Details Card */} + + + + + + + + + + + + + + {/* Domain Card */} + + + + + + + + + + +
+ + {/* Requirements Table */} +
+
+
+ + + + + + + + + + + + + + + + {["type", "description", "status"].map((requirementType) => ( + + + + + + + + + + + + ))} + +
+
+
+
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/page.tsx similarity index 100% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/overview/frameworks/controls/[id]/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/frameworks/controls/[id]/page.tsx diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/layout.tsx new file mode 100644 index 0000000000..27b4f2bb99 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/layout.tsx @@ -0,0 +1,32 @@ +import { auth } from "@/auth"; +import { getI18n } from "@/locales/server"; +import { redirect } from "next/navigation"; +import { SecondaryMenu } from "@bubba/ui/secondary-menu"; + +export default async function Layout({ + children, +}: { + children: React.ReactNode; +}) { + const t = await getI18n(); + const session = await auth(); + + if (!session?.user?.organizationId) { + redirect("/auth"); + } + + return ( +
+ + +
{children}
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/loading.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/loading.tsx new file mode 100644 index 0000000000..d307ac1ee6 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/loading.tsx @@ -0,0 +1,70 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; +import { Skeleton } from "@bubba/ui/skeleton"; + +export default function Loading() { + return ( +
+
+ {/* Framework Progress Card Skeleton */} + + + + + + + +
+ {/* Main compliance circle */} +
+ +
+ + {/* Three smaller circles */} +
+ {["policies", "evidence", "tests"].map((item) => ( +
+ + +
+ ))} +
+
+
+
+ + {/* Requirements Status Card Skeleton */} + + + + + + + +
+
+ {["framework1", "framework2", "framework3"].map((item) => ( +
+ +
+
+ + +
+ +
+
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/page.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/page.tsx similarity index 59% rename from apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/page.tsx rename to apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/page.tsx index 8191e2b662..a9555f048f 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/(home)/page.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/[orgId]/overview/page.tsx @@ -1,15 +1,17 @@ +import { auth } from "@/auth"; import { getI18n } from "@/locales/server"; import type { Metadata } from "next"; import { setStaticParamsLocale } from "next-international/server"; -import { FrameworksOverview } from "./components/FrameworksOverview"; -import { db } from "@bubba/db"; -import { auth } from "@/auth"; import { redirect } from "next/navigation"; +import { FrameworksOverview } from "./components/FrameworksOverview"; +import { getFrameworks } from "./data/getFrameworks"; +import { getComplianceScores } from "./data/getComplianceScores"; +import { getFrameworkCategories } from "./data/getFrameworkCategories"; export default async function DashboardPage({ params, }: { - params: Promise<{ locale: string }>; + params: Promise<{ locale: string; orgId: string }>; }) { const { locale } = await params; setStaticParamsLocale(locale); @@ -22,8 +24,22 @@ export default async function DashboardPage({ } const frameworks = await getFrameworks(organizationId); - - return ; + const complianceScores = await getComplianceScores( + organizationId, + frameworks, + ); + const frameworksWithCompliance = await getFrameworkCategories( + organizationId, + frameworks, + ); + + return ( + + ); } export async function generateMetadata({ @@ -39,15 +55,3 @@ export async function generateMetadata({ title: t("sidebar.overview"), }; } - -const getFrameworks = async (organizationId: string) => { - const frameworks = await db.organizationFramework.findMany({ - where: { organizationId: organizationId }, - include: { - organizationControl: true, - framework: true, - }, - }); - - return frameworks; -}; diff --git a/apps/app/src/app/[locale]/page.tsx b/apps/app/src/app/[locale]/page.tsx index e0ead87c27..b6f710ceee 100644 --- a/apps/app/src/app/[locale]/page.tsx +++ b/apps/app/src/app/[locale]/page.tsx @@ -9,7 +9,7 @@ export default async function RootPage() { } if (session.user?.organizationId) { - redirect(`/${session.user.organizationId}`); + redirect(`/${session.user.organizationId}/overview`); } redirect("/setup"); diff --git a/apps/app/src/components/main-menu.tsx b/apps/app/src/components/main-menu.tsx index f6d9e1254f..a4ca333e5b 100644 --- a/apps/app/src/components/main-menu.tsx +++ b/apps/app/src/components/main-menu.tsx @@ -190,7 +190,7 @@ export function MainMenu({ const defaultItems: MenuItem[] = [ { id: "overview", - path: "/:organizationId", + path: "/:organizationId/overview", name: t("sidebar.overview"), disabled: false, icon: Icons.Overview, diff --git a/apps/app/src/hooks/use-compliance-scores.ts b/apps/app/src/hooks/use-compliance-scores.ts deleted file mode 100644 index 94e9d80726..0000000000 --- a/apps/app/src/hooks/use-compliance-scores.ts +++ /dev/null @@ -1,222 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import { usePolicies } from "@/app/[locale]/(app)/(dashboard)/[orgId]/policies/all/(overview)/hooks/usePolicies"; -import { useTests } from "@/app/[locale]/(app)/(dashboard)/[orgId]/tests/all/hooks/useTests"; -import type { - Framework, - OrganizationControl, - OrganizationFramework, -} from "@bubba/db/types"; -import { useOrganizationEvidenceTasks } from "@/app/[locale]/(app)/(dashboard)/[orgId]/evidence/hooks/useEvidenceTasks"; -import { usePoliciesByFramework } from "./use-policies-by-framework"; - -interface UseComplianceScoresProps { - frameworks?: (OrganizationFramework & { - framework: Framework; - organizationControl: OrganizationControl[]; - })[]; -} - -export function useComplianceScores({ - frameworks = [], -}: UseComplianceScoresProps = {}) { - // Getting ALL policies. - const { policies = [], isLoading: policiesLoading } = usePolicies({ - page: 1, - pageSize: 1000, - }); - - // Get policies by framework. - const { policiesByFramework = [], isLoading: frameworksLoading } = - usePoliciesByFramework(); - - // Getting ALL evidence tasks. - const { data: evidenceTasks = [], isLoading: evidenceTasksLoading } = - useOrganizationEvidenceTasks({ - page: 1, - pageSize: 1000, - }); - - // Create evidenceByFramework array for clearer separation of concerns - const evidenceByFramework = useMemo(() => { - // Map evidence tasks to their frameworks - return evidenceTasks - .filter((task) => task.frameworkId) // Only include tasks with a frameworkId - .map((task) => ({ - id: task.id, - frameworkId: task.frameworkId, - evidenceId: task.id, - published: task.published, - })); - }, [evidenceTasks]); - - // Getting ALL tests. - const { tests = [], isLoading: testsLoading } = useTests(''); - - // Combined loading state - const isLoading = policiesLoading || evidenceTasksLoading || testsLoading; - - // Calculate all compliance scores - const scores = useMemo(() => { - // Calculate policies compliance (checking for published status) - const policiesCompliance = - policies.length > 0 - ? Math.round( - (policies.filter((p) => p.status === "published").length / - policies.length) * - 100 - ) - : 0; - - // Calculate evidence tasks compliance (checking for published status) - const evidenceTasksCompliance = - evidenceTasks.length > 0 - ? Math.round( - (evidenceTasks.filter((task) => task.published === true).length / - evidenceTasks.length) * - 100 - ) - : 0; - - // Calculate cloud tests compliance (tests with "passed" result) - const cloudTestsCompliance = - tests.length > 0 - ? Math.round( - (tests.filter((t) => t.result === "passed").length / tests.length) * - 100 - ) - : 0; - - // Calculate framework-specific compliance - const frameworkCompliance = frameworks.map((framework) => { - // Calculate framework controls compliance - const totalControls = framework.organizationControl.length; - - // Count controls with any progress (in_progress or compliant) - const inProgressControls = framework.organizationControl.filter( - (control) => control.status === "in_progress" - ).length; - - const compliantControls = framework.organizationControl.filter( - (control) => control.status === "compliant" - ).length; - - // For compliance percentage, count both in_progress (partial) and compliant (full) - // Give in_progress controls half weight compared to compliant ones - const progressWeight = 0.5; // Weight for in_progress controls - const weightedProgress = - compliantControls + inProgressControls * progressWeight; - - // Calculate compliance percentage based on controls - const controlsCompliance = - totalControls > 0 - ? Math.round((weightedProgress / totalControls) * 100) - : 0; - - // Find policies for this specific framework using policiesByFramework - const frameworkPolicies = policiesByFramework.filter( - (policy) => policy.frameworkId === framework.framework.id - ); - - // Calculate framework-specific policies compliance - // We need to look up the policy status in the main policies array since policiesByFramework might not have it directly - const frameworkPoliciesCompliance = - frameworkPolicies.length > 0 - ? Math.round( - (frameworkPolicies.filter((frameworkPolicy) => { - // Find the corresponding policy in the main policies array - const policy = policies.find( - (p) => p.id === frameworkPolicy.policyId - ); - // Check if it's published - return policy?.status === "published"; - }).length / - frameworkPolicies.length) * - 100 - ) - : null; - - // Find evidence tasks related to this framework using evidenceByFramework - const frameworkEvidenceTasks = evidenceByFramework.filter( - (evidence) => evidence.frameworkId === framework.framework.id - ); - - // Calculate framework-specific evidence tasks compliance - const frameworkEvidenceTasksCompliance = - frameworkEvidenceTasks.length > 0 - ? Math.round( - (frameworkEvidenceTasks.filter( - (evidence) => evidence.published === true - ).length / - frameworkEvidenceTasks.length) * - 100 - ) - : null; - - // Calculate overall framework compliance as average of all components with data - const complianceScores = [controlsCompliance]; - if (frameworkPoliciesCompliance !== null) - complianceScores.push(frameworkPoliciesCompliance); - if (frameworkEvidenceTasksCompliance !== null) - complianceScores.push(frameworkEvidenceTasksCompliance); - - const totalCompliance = - complianceScores.length > 0 - ? Math.round( - complianceScores.reduce((sum, score) => sum + score, 0) / - complianceScores.length - ) - : controlsCompliance; - - return { - id: framework.framework.id, - name: framework.framework.name, - compliance: totalCompliance, - }; - }); - - // Calculate overall compliance as the average of all available scores - const calculateOverallCompliance = () => { - // Count how many categories have data - const categoriesWithData = [ - policies.length > 0, - evidenceTasks.length > 0, - tests.length > 0, - ].filter(Boolean).length; - - // If no categories have data, return 0 - if (categoriesWithData === 0) return 0; - - // Calculate the sum of all compliance scores - const totalScore = - policiesCompliance + evidenceTasksCompliance + cloudTestsCompliance; - - // Return the average, rounded to the nearest integer - return Math.round(totalScore / categoriesWithData); - }; - - return { - policiesCompliance, - evidenceTasksCompliance, - cloudTestsCompliance, - overallCompliance: calculateOverallCompliance(), - frameworkCompliance, - }; - }, [ - policies, - evidenceTasks, - tests, - frameworks, - policiesByFramework, - evidenceByFramework, - ]); - - return { - ...scores, - isLoading, - policies, - evidenceTasks, - tests, - }; -} diff --git a/packages/db/prisma/seed.js b/packages/db/prisma/seed.js index 919a3ce9ee..1bfa92e08b 100644 --- a/packages/db/prisma/seed.js +++ b/packages/db/prisma/seed.js @@ -1,9 +1,7 @@ "use strict"; -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); const client_1 = require("@prisma/client"); const client_2 = require("@prisma/client"); @@ -13,576 +11,487 @@ const node_fs_2 = __importDefault(require("node:fs")); const data_1 = require("@bubba/data"); const prisma = new client_1.PrismaClient(); async function main() { - if (process.env.NODE_ENV === "development") { - console.log("\nšŸ—‘ļø Cleaning up existing data..."); - await prisma.organizationFramework.deleteMany(); - await prisma.organizationCategory.deleteMany(); - await prisma.organizationControl.deleteMany(); - await prisma.organizationPolicy.deleteMany(); - await prisma.organizationControlRequirement.deleteMany(); - await prisma.organizationEvidence.deleteMany(); - await prisma.policy.deleteMany(); - await prisma.policyControl.deleteMany(); - await prisma.policyFramework.deleteMany(); - await prisma.control.deleteMany(); - await prisma.controlRequirement.deleteMany(); - await prisma.framework.deleteMany(); - await prisma.frameworkCategory.deleteMany(); - await prisma.evidence.deleteMany(); - console.log("āœ… Database cleaned"); - } - console.log("\nšŸ“‹ Seeding policies..."); - await seedPolicies(); - console.log("āœ… Policies seeded"); - console.log("\nšŸ”— Seeding evidence records (phase 1)"); - await seedEvidenceRecords(); - console.log("āœ… Evidence records seeded"); - console.log("\nšŸ—ļø Seeding frameworks..."); - await seedFrameworks(); - console.log("āœ… Frameworks seeded"); - console.log("\nšŸ”— Seeding policy frameworks..."); - await seedPolicyFramework(); - console.log("āœ… Policy frameworks seeded"); - console.log("\nšŸ”„ Updating policy links (phase 1)"); - await updatePolicyLinks(); - console.log("āœ… Policy links updated"); - console.log("\nšŸ”„ Updating evidence links (phase 2)"); - await updateEvidenceLinks(); - console.log("āœ… Evidence links updated"); - console.log("\nšŸŽ„ Seeding training videos..."); - await seedTrainingVideos(); - console.log("āœ… Training videos seeded"); - console.log("\nšŸŽ‰ All data seeded successfully!"); + if (process.env.NODE_ENV === "development") { + console.log("\nšŸ—‘ļø Cleaning up existing data..."); + await prisma.organizationFramework.deleteMany(); + await prisma.organizationCategory.deleteMany(); + await prisma.organizationControl.deleteMany(); + await prisma.organizationPolicy.deleteMany(); + await prisma.organizationControlRequirement.deleteMany(); + await prisma.organizationEvidence.deleteMany(); + await prisma.policy.deleteMany(); + await prisma.policyControl.deleteMany(); + await prisma.policyFramework.deleteMany(); + await prisma.control.deleteMany(); + await prisma.controlRequirement.deleteMany(); + await prisma.framework.deleteMany(); + await prisma.frameworkCategory.deleteMany(); + await prisma.evidence.deleteMany(); + console.log("āœ… Database cleaned"); + } + console.log("\nšŸ“‹ Seeding policies..."); + await seedPolicies(); + console.log("āœ… Policies seeded"); + console.log("\nšŸ”— Seeding evidence records (phase 1)"); + await seedEvidenceRecords(); + console.log("āœ… Evidence records seeded"); + console.log("\nšŸ—ļø Seeding frameworks..."); + await seedFrameworks(); + console.log("āœ… Frameworks seeded"); + console.log("\nšŸ”— Seeding policy frameworks..."); + await seedPolicyFramework(); + console.log("āœ… Policy frameworks seeded"); + console.log("\nšŸ”„ Updating policy links (phase 1)"); + await updatePolicyLinks(); + console.log("āœ… Policy links updated"); + console.log("\nšŸ”„ Updating evidence links (phase 2)"); + await updateEvidenceLinks(); + console.log("āœ… Evidence links updated"); + console.log("\nšŸŽ„ Seeding training videos..."); + await seedTrainingVideos(); + console.log("āœ… Training videos seeded"); + console.log("\nšŸŽ‰ All data seeded successfully!"); } main() - .catch((e) => { + .catch((e) => { console.error("\nāŒ Error during seeding:", e); process.exit(1); - }) - .finally(async () => { +}) + .finally(async () => { await prisma.$disconnect(); - }); +}); async function seedPolicies() { - const policiesDir = (0, node_path_1.join)(__dirname, "../../data/policies"); - const policyFiles = (0, node_fs_1.readdirSync)(policiesDir).filter((file) => - file.endsWith(".json") - ); - console.log(`šŸ“„ Found ${policyFiles.length} policy files to process`); - for (const file of policyFiles) { - console.log(` ā³ Processing ${file}...`); - try { - const fileContent = (0, node_fs_1.readFileSync)( - (0, node_path_1.join)(policiesDir, file), - "utf8" - ); - const policyData = JSON.parse(fileContent); - // Check for any existing policies with the same slug - const existingPolicyWithSlug = await prisma.policy.findFirst({ - where: { - slug: policyData.metadata.slug, - NOT: { id: policyData.metadata.id }, - }, - }); - // If there's a conflict, delete the existing policy - if (existingPolicyWithSlug) { - console.log( - ` āš ļø Found existing policy with slug "${policyData.metadata.slug}", replacing it...` - ); - await prisma.policy.delete({ - where: { id: existingPolicyWithSlug.id }, - }); - } - // Now we can safely upsert the new policy - await prisma.policy.upsert({ - where: { - id: policyData.metadata.id, - }, - update: { - name: policyData.metadata.name, - slug: policyData.metadata.slug, - description: policyData.metadata.description, - content: policyData.content, - usedBy: policyData.metadata.usedBy, - frequency: policyData.metadata?.frequency ?? null, - department: - policyData.metadata?.department ?? client_1.Departments.none, - }, - create: { - id: policyData.metadata.id, - slug: policyData.metadata.slug, - name: policyData.metadata.name, - description: policyData.metadata.description, - content: policyData.content, - usedBy: policyData.metadata.usedBy, - frequency: policyData.metadata?.frequency ?? null, - department: - policyData.metadata?.department ?? client_1.Departments.none, - }, - }); - console.log(` āœ… ${file} processed`); - } catch (error) { - console.error(` āŒ Error processing ${file}:`, error); - if (error instanceof Error) { - console.error(` Error details: ${error.message}`); - } + const policiesDir = (0, node_path_1.join)(__dirname, "../../data/policies"); + const policyFiles = (0, node_fs_1.readdirSync)(policiesDir).filter((file) => file.endsWith(".json")); + console.log(`šŸ“„ Found ${policyFiles.length} policy files to process`); + for (const file of policyFiles) { + console.log(` ā³ Processing ${file}...`); + try { + const fileContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(policiesDir, file), "utf8"); + const policyData = JSON.parse(fileContent); + // Check for any existing policies with the same slug + const existingPolicyWithSlug = await prisma.policy.findFirst({ + where: { + slug: policyData.metadata.slug, + NOT: { id: policyData.metadata.id }, + }, + }); + // If there's a conflict, delete the existing policy + if (existingPolicyWithSlug) { + console.log(` āš ļø Found existing policy with slug "${policyData.metadata.slug}", replacing it...`); + await prisma.policy.delete({ + where: { id: existingPolicyWithSlug.id }, + }); + } + // Now we can safely upsert the new policy + await prisma.policy.upsert({ + where: { + id: policyData.metadata.id, + }, + update: { + name: policyData.metadata.name, + slug: policyData.metadata.slug, + description: policyData.metadata.description, + content: policyData.content, + usedBy: policyData.metadata.usedBy, + frequency: policyData.metadata?.frequency ?? null, + department: policyData.metadata?.department ?? client_1.Departments.none, + }, + create: { + id: policyData.metadata.id, + slug: policyData.metadata.slug, + name: policyData.metadata.name, + description: policyData.metadata.description, + content: policyData.content, + usedBy: policyData.metadata.usedBy, + frequency: policyData.metadata?.frequency ?? null, + department: policyData.metadata?.department ?? client_1.Departments.none, + }, + }); + console.log(` āœ… ${file} processed`); + } + catch (error) { + console.error(` āŒ Error processing ${file}:`, error); + if (error instanceof Error) { + console.error(` Error details: ${error.message}`); + } + } } - } } async function seedFrameworks() { - const frameworksFile = (0, node_path_1.join)( - __dirname, - "../../data/frameworks.json" - ); - const frameworksJson = JSON.parse( - (0, node_fs_1.readFileSync)(frameworksFile, "utf8") - ); - console.log( - `šŸ” Found ${Object.keys(frameworksJson).length} frameworks to process` - ); - // Populate the app level frameworks that every org has access to. - for (const [frameworkId, frameworkData] of Object.entries(frameworksJson)) { - console.log(` ā³ Processing framework: ${frameworkData.name}...`); - // First, upsert the framework itself. - const insertedFramework = await prisma.framework.upsert({ - where: { id: frameworkId }, - update: { - description: frameworkData.description, - version: frameworkData.version, - }, - create: { - id: frameworkId, - name: frameworkData.name, - description: frameworkData.description, - version: frameworkData.version, - }, - }); - // Then, upsert the framework categories. - await seedFrameworkCategories(insertedFramework.id); - console.log(` āœ… Framework ${frameworkData.name} processed`); - } + const frameworksFile = (0, node_path_1.join)(__dirname, "../../data/frameworks.json"); + const frameworksJson = JSON.parse((0, node_fs_1.readFileSync)(frameworksFile, "utf8")); + console.log(`šŸ” Found ${Object.keys(frameworksJson).length} frameworks to process`); + // Populate the app level frameworks that every org has access to. + for (const [frameworkId, frameworkData] of Object.entries(frameworksJson)) { + console.log(` ā³ Processing framework: ${frameworkData.name}...`); + // First, upsert the framework itself. + const insertedFramework = await prisma.framework.upsert({ + where: { id: frameworkId }, + update: { + description: frameworkData.description, + version: frameworkData.version, + }, + create: { + id: frameworkId, + name: frameworkData.name, + description: frameworkData.description, + version: frameworkData.version, + }, + }); + // Then, upsert the framework categories. + await seedFrameworkCategories(insertedFramework.id); + console.log(` āœ… Framework ${frameworkData.name} processed`); + } } async function seedFrameworkCategories(frameworkId) { - let categories; - try { - categories = node_fs_2.default.readFileSync( - (0, node_path_1.join)( - __dirname, - `../../data/categories/${frameworkId}.json` - ), - "utf8" - ); - } catch (error) { - console.log( - ` āš ļø No categories found for framework ${frameworkId}, skipping` - ); - return; - } - const categoriesData = JSON.parse(categories); - console.log( - ` šŸ“‘ Found ${Object.keys(categoriesData).length} categories for ${frameworkId}` - ); - // Upsert the framework categories for the given framework. - for (const [categoryCode, categoryData] of Object.entries(categoriesData)) { - console.log(` ā³ Processing category: ${categoryData.name}...`); - // First, upsert the framework category itself for the given framework. - await prisma.frameworkCategory.upsert({ - where: { id: categoryCode }, - update: { - name: categoryData.name, - code: categoryData.code, - description: categoryData.description, - frameworkId: frameworkId, - }, - create: { - id: categoryCode, - name: categoryData.name, - description: categoryData.description, - code: categoryData.code, - frameworkId: frameworkId, - }, - }); - // Then, upsert the controls for the given framework category. - await seedFrameworkCategoryControls(frameworkId, categoryCode); - console.log(` āœ… Category ${categoryData.name} processed`); - } + let categories; + try { + categories = node_fs_2.default.readFileSync((0, node_path_1.join)(__dirname, `../../data/categories/${frameworkId}.json`), "utf8"); + } + catch (error) { + console.log(` āš ļø No categories found for framework ${frameworkId}, skipping`); + return; + } + const categoriesData = JSON.parse(categories); + console.log(` šŸ“‘ Found ${Object.keys(categoriesData).length} categories for ${frameworkId}`); + // Upsert the framework categories for the given framework. + for (const [categoryCode, categoryData] of Object.entries(categoriesData)) { + console.log(` ā³ Processing category: ${categoryData.name}...`); + // First, upsert the framework category itself for the given framework. + await prisma.frameworkCategory.upsert({ + where: { id: categoryCode }, + update: { + name: categoryData.name, + code: categoryData.code, + description: categoryData.description, + frameworkId: frameworkId, + }, + create: { + id: categoryCode, + name: categoryData.name, + description: categoryData.description, + code: categoryData.code, + frameworkId: frameworkId, + }, + }); + // Then, upsert the controls for the given framework category. + await seedFrameworkCategoryControls(frameworkId, categoryCode); + console.log(` āœ… Category ${categoryData.name} processed`); + } } async function seedFrameworkCategoryControls(frameworkId, categoryCode) { - const controls = node_fs_2.default.readFileSync( - (0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`), - "utf8" - ); - const controlsData = JSON.parse(controls); - const filteredControlsData = Object.fromEntries( - Object.entries(controlsData).filter( - ([_, data]) => data.categoryId === categoryCode - ) - ); - console.log( - ` šŸŽ® Processing ${Object.keys(filteredControlsData).length} controls` - ); - for (const [controlCode, controlData] of Object.entries( - filteredControlsData - )) { - // First, upsert the controls itself for the given category. - await prisma.control.upsert({ - where: { code: controlCode }, - update: { - name: controlData.name, - description: controlData.description, - domain: controlData.domain, - frameworkCategoryId: categoryCode, - }, - create: { - // Use the control code (e.g. CC1.1) as both the id and code - id: controlCode, - code: controlCode, - name: controlData.name, - description: controlData.description, - domain: controlData.domain, - frameworkCategoryId: categoryCode, - }, - }); - // Then, upsert the requirements for the given control. - console.log( - ` šŸ“ Processing ${controlData.requirements.length} requirements for ${controlCode}` - ); - for (const requirement of controlData.requirements) { - // For both policy and evidence requirements, initially set policyId and evidenceId to null - // They will be updated later in their respective update functions - await prisma.controlRequirement.upsert({ - where: { - id: requirement.id, - }, - create: { - id: requirement.id, - controlId: controlCode, - name: requirement.name || "", - type: requirement.type, - description: requirement.description || "", - // Set both policyId and evidenceId to null initially - policyId: null, - evidenceId: null, - frequency: requirement?.frequency ?? null, - department: requirement?.department ?? client_1.Departments.none, - }, - update: { - name: requirement.name || "", - description: requirement.description || "", - // Don't update policyId or evidenceId here - frequency: requirement?.frequency ?? null, - department: requirement?.department ?? client_1.Departments.none, - }, - }); + const controls = node_fs_2.default.readFileSync((0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`), "utf8"); + const controlsData = JSON.parse(controls); + const filteredControlsData = Object.fromEntries(Object.entries(controlsData).filter(([_, data]) => data.categoryId === categoryCode)); + console.log(` šŸŽ® Processing ${Object.keys(filteredControlsData).length} controls`); + for (const [controlCode, controlData] of Object.entries(filteredControlsData)) { + // First, upsert the controls itself for the given category. + await prisma.control.upsert({ + where: { code: controlCode }, + update: { + name: controlData.name, + description: controlData.description, + domain: controlData.domain, + frameworkCategoryId: categoryCode, + }, + create: { + // Use the control code (e.g. CC1.1) as both the id and code + id: controlCode, + code: controlCode, + name: controlData.name, + description: controlData.description, + domain: controlData.domain, + frameworkCategoryId: categoryCode, + }, + }); + // Then, upsert the requirements for the given control. + console.log(` šŸ“ Processing ${controlData.requirements.length} requirements for ${controlCode}`); + for (const requirement of controlData.requirements) { + // For both policy and evidence requirements, initially set policyId and evidenceId to null + // They will be updated later in their respective update functions + await prisma.controlRequirement.upsert({ + where: { + id: requirement.id, + }, + create: { + id: requirement.id, + controlId: controlCode, + name: requirement.name || "", + type: requirement.type, + description: requirement.description || "", + // Set both policyId and evidenceId to null initially + policyId: null, + evidenceId: null, + frequency: requirement?.frequency ?? null, + department: requirement?.department ?? client_1.Departments.none, + }, + update: { + name: requirement.name || "", + description: requirement.description || "", + // Don't update policyId or evidenceId here + frequency: requirement?.frequency ?? null, + department: requirement?.department ?? client_1.Departments.none, + }, + }); + } } - } } async function seedPolicyFramework() { - const policies = await prisma.policy.findMany(); - console.log( - `šŸ”„ Processing ${policies.length} policies for framework mapping` - ); - for (const policy of policies) { - console.log(` ā³ Mapping policy: ${policy.name}...`); - if (!policy.usedBy) { - console.log(` āš ļø Policy ${policy.name} has no usedBy, skipping`); - continue; + const policies = await prisma.policy.findMany(); + console.log(`šŸ”„ Processing ${policies.length} policies for framework mapping`); + for (const policy of policies) { + console.log(` ā³ Mapping policy: ${policy.name}...`); + if (!policy.usedBy) { + console.log(` āš ļø Policy ${policy.name} has no usedBy, skipping`); + continue; + } + for (const [frameworkId, controlCodes] of Object.entries(policy.usedBy)) { + // First verify the framework exists + const framework = await prisma.framework.findUnique({ + where: { id: frameworkId }, + }); + if (!framework) { + console.log(` āš ļø Framework ${frameworkId} not found, skipping`); + continue; + } + // Upsert the policy framework mapping + await prisma.policyFramework.upsert({ + where: { id: `${frameworkId}_${policy.id}` }, + update: { + policyId: policy.id, + frameworkId: frameworkId, + }, + create: { + id: `${frameworkId}_${policy.id}`, + policyId: policy.id, + frameworkId: frameworkId, + }, + }); + // For each control code, create the policy control mapping directly + for (const controlCode of controlCodes) { + console.log(` ā³ Mapping control ${controlCode} to policy ${policy.name}`); + // Now create the policy control mapping using the control code directly + await prisma.policyControl.upsert({ + where: { + id: `${frameworkId}_${policy.id}_${controlCode}`, + }, + update: { + policyId: policy.id, + controlId: controlCode, // Use the control code directly + }, + create: { + id: `${frameworkId}_${policy.id}_${controlCode}`, + policyId: policy.id, + controlId: controlCode, // Use the control code directly + }, + }); + } + } + console.log(` āœ… Policy ${policy.name} mapped`); } - for (const [frameworkId, controlCodes] of Object.entries(policy.usedBy)) { - // First verify the framework exists - const framework = await prisma.framework.findUnique({ - where: { id: frameworkId }, - }); - if (!framework) { - console.log(` āš ļø Framework ${frameworkId} not found, skipping`); - continue; - } - // Upsert the policy framework mapping - await prisma.policyFramework.upsert({ - where: { id: `${frameworkId}_${policy.id}` }, - update: { - policyId: policy.id, - frameworkId: frameworkId, - }, - create: { - id: `${frameworkId}_${policy.id}`, - policyId: policy.id, - frameworkId: frameworkId, - }, - }); - // For each control code, create the policy control mapping directly - for (const controlCode of controlCodes) { - console.log( - ` ā³ Mapping control ${controlCode} to policy ${policy.name}` - ); - // Now create the policy control mapping using the control code directly - await prisma.policyControl.upsert({ - where: { - id: `${frameworkId}_${policy.id}_${controlCode}`, - }, - update: { - policyId: policy.id, - controlId: controlCode, // Use the control code directly - }, - create: { - id: `${frameworkId}_${policy.id}_${controlCode}`, - policyId: policy.id, - controlId: controlCode, // Use the control code directly - }, - }); - } - } - console.log(` āœ… Policy ${policy.name} mapped`); - } } // Phase 1: Create evidence records from files (without linking to requirements) async function seedEvidenceRecords() { - const evidenceDir = (0, node_path_1.join)(__dirname, "../../data/evidence"); - const evidenceFiles = (0, node_fs_1.readdirSync)(evidenceDir).filter((file) => - file.endsWith(".json") - ); - console.log(`šŸ“„ Found ${evidenceFiles.length} evidence files to process`); - for (const file of evidenceFiles) { - const evidenceId = file.replace(".json", ""); - console.log(` ā³ Processing evidence file: ${file}...`); - try { - const fileContent = (0, node_fs_1.readFileSync)( - (0, node_path_1.join)(evidenceDir, file), - "utf8" - ); - const evidenceData = JSON.parse(fileContent); - // Upsert the evidence record - await prisma.evidence.upsert({ - where: { - id: evidenceData.id, - }, - update: { - name: evidenceData.name, - description: evidenceData.description, - frequency: evidenceData.frequency ?? null, - department: evidenceData.department ?? client_1.Departments.none, - }, - create: { - id: evidenceData.id, - name: evidenceData.name, - description: evidenceData.description, - frequency: evidenceData.frequency ?? null, - department: evidenceData.department ?? client_1.Departments.none, - }, - }); - console.log(` āœ… Evidence ${evidenceId} processed`); - } catch (error) { - console.error(` āŒ Error processing ${file}:`, error); - if (error instanceof Error) { - console.error(` Error details: ${error.message}`); - } + const evidenceDir = (0, node_path_1.join)(__dirname, "../../data/evidence"); + const evidenceFiles = (0, node_fs_1.readdirSync)(evidenceDir).filter((file) => file.endsWith(".json")); + console.log(`šŸ“„ Found ${evidenceFiles.length} evidence files to process`); + for (const file of evidenceFiles) { + const evidenceId = file.replace(".json", ""); + console.log(` ā³ Processing evidence file: ${file}...`); + try { + const fileContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDir, file), "utf8"); + const evidenceData = JSON.parse(fileContent); + // Upsert the evidence record + await prisma.evidence.upsert({ + where: { + id: evidenceData.id, + }, + update: { + name: evidenceData.name, + description: evidenceData.description, + frequency: evidenceData.frequency ?? null, + department: evidenceData.department ?? client_1.Departments.none, + }, + create: { + id: evidenceData.id, + name: evidenceData.name, + description: evidenceData.description, + frequency: evidenceData.frequency ?? null, + department: evidenceData.department ?? client_1.Departments.none, + }, + }); + console.log(` āœ… Evidence ${evidenceId} processed`); + } + catch (error) { + console.error(` āŒ Error processing ${file}:`, error); + if (error instanceof Error) { + console.error(` Error details: ${error.message}`); + } + } } - } } // Phase 2: Update control requirements to link to evidence async function updateEvidenceLinks() { - // Get all control requirements that are evidence type - const evidenceRequirements = await prisma.controlRequirement.findMany({ - where: { - type: client_2.RequirementType.evidence, - }, - }); - console.log( - `šŸ”„ Processing ${evidenceRequirements.length} evidence requirements` - ); - for (const requirement of evidenceRequirements) { - // Get the controls file for this requirement to extract the evidenceId - const control = await prisma.control.findUnique({ - where: { id: requirement.controlId }, - include: { frameworkCategory: true }, - }); - if (!control) { - console.log( - ` āš ļø Control not found for requirement ${requirement.id}, skipping` - ); - continue; - } - if (!control.frameworkCategory) { - console.log( - ` āš ļø Framework category not found for control ${control.id}, skipping` - ); - continue; - } - // Get the framework ID from the category - const frameworkId = control.frameworkCategory.frameworkId; - // Get the controls data from the file - const controlsFile = (0, node_path_1.join)( - __dirname, - `../../data/controls/${frameworkId}.json` - ); - const controlsData = JSON.parse( - node_fs_2.default.readFileSync(controlsFile, "utf8") - ); - // Find the requirement in the control data - const controlData = controlsData[control.code]; - if (!controlData) { - console.log( - ` āš ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping` - ); - continue; - } - const reqData = controlData.requirements.find( - (req) => req.id === requirement.id - ); - if (!reqData) { - console.log( - ` āš ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping` - ); - continue; - } - // Get the evidenceId from the requirement data - const evidenceId = reqData.evidenceId; - if (!evidenceId) { - console.log( - ` āš ļø No evidenceId found for requirement ${requirement.id}, skipping` - ); - continue; - } - // Verify the evidence exists - const evidence = await prisma.evidence.findUnique({ - where: { id: evidenceId }, + // Get all control requirements that are evidence type + const evidenceRequirements = await prisma.controlRequirement.findMany({ + where: { + type: client_2.RequirementType.evidence, + }, }); - if (!evidence) { - console.log( - ` āš ļø Evidence ${evidenceId} not found for requirement ${requirement.id}, skipping` - ); - continue; + console.log(`šŸ”„ Processing ${evidenceRequirements.length} evidence requirements`); + for (const requirement of evidenceRequirements) { + // Get the controls file for this requirement to extract the evidenceId + const control = await prisma.control.findUnique({ + where: { id: requirement.controlId }, + include: { frameworkCategory: true }, + }); + if (!control) { + console.log(` āš ļø Control not found for requirement ${requirement.id}, skipping`); + continue; + } + if (!control.frameworkCategory) { + console.log(` āš ļø Framework category not found for control ${control.id}, skipping`); + continue; + } + // Get the framework ID from the category + const frameworkId = control.frameworkCategory.frameworkId; + // Get the controls data from the file + const controlsFile = (0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`); + const controlsData = JSON.parse(node_fs_2.default.readFileSync(controlsFile, "utf8")); + // Find the requirement in the control data + const controlData = controlsData[control.code]; + if (!controlData) { + console.log(` āš ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping`); + continue; + } + const reqData = controlData.requirements.find((req) => req.id === requirement.id); + if (!reqData) { + console.log(` āš ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping`); + continue; + } + // Get the evidenceId from the requirement data + const evidenceId = reqData.evidenceId; + if (!evidenceId) { + console.log(` āš ļø No evidenceId found for requirement ${requirement.id}, skipping`); + continue; + } + // Verify the evidence exists + const evidence = await prisma.evidence.findUnique({ + where: { id: evidenceId }, + }); + if (!evidence) { + console.log(` āš ļø Evidence ${evidenceId} not found for requirement ${requirement.id}, skipping`); + continue; + } + console.log(` ā³ Linking requirement ${requirement.id} to evidence ${evidenceId}...`); + // Update the control requirement to link to the evidence + await prisma.controlRequirement.update({ + where: { + id: requirement.id, + }, + data: { + evidenceId: evidenceId, + name: evidence.name, + description: evidence.description, + frequency: evidence.frequency, + department: evidence.department, + }, + }); + console.log(` āœ… Requirement ${requirement.id} linked to evidence ${evidenceId}`); } - console.log( - ` ā³ Linking requirement ${requirement.id} to evidence ${evidenceId}...` - ); - // Update the control requirement to link to the evidence - await prisma.controlRequirement.update({ - where: { - id: requirement.id, - }, - data: { - evidenceId: evidenceId, - name: evidence.name, - description: evidence.description, - frequency: evidence.frequency, - department: evidence.department, - }, - }); - console.log( - ` āœ… Requirement ${requirement.id} linked to evidence ${evidenceId}` - ); - } } // Phase 1: Update control requirements to link to policies async function updatePolicyLinks() { - // Get all control requirements that are policy type - const policyRequirements = await prisma.controlRequirement.findMany({ - where: { - type: client_2.RequirementType.policy, - }, - }); - console.log(`šŸ”„ Processing ${policyRequirements.length} policy requirements`); - for (const requirement of policyRequirements) { - // Get the controls file for this requirement to extract the policyId - const control = await prisma.control.findUnique({ - where: { id: requirement.controlId }, - include: { frameworkCategory: true }, - }); - if (!control) { - console.log( - ` āš ļø Control not found for requirement ${requirement.id}, skipping` - ); - continue; - } - if (!control.frameworkCategory) { - console.log( - ` āš ļø Framework category not found for control ${control.id}, skipping` - ); - continue; - } - // Get the framework ID from the category - const frameworkId = control.frameworkCategory.frameworkId; - // Get the controls data from the file - const controlsFile = (0, node_path_1.join)( - __dirname, - `../../data/controls/${frameworkId}.json` - ); - const controlsData = JSON.parse( - node_fs_2.default.readFileSync(controlsFile, "utf8") - ); - // Find the requirement in the control data - const controlData = controlsData[control.code]; - if (!controlData) { - console.log( - ` āš ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping` - ); - continue; - } - const reqData = controlData.requirements.find( - (req) => req.id === requirement.id - ); - if (!reqData) { - console.log( - ` āš ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping` - ); - continue; - } - // Get the policyId from the requirement data - const policyId = reqData.policyId; - if (!policyId) { - console.log( - ` āš ļø No policyId found for requirement ${requirement.id}, skipping` - ); - continue; - } - // Verify the policy exists - const policy = await prisma.policy.findUnique({ - where: { id: policyId }, + // Get all control requirements that are policy type + const policyRequirements = await prisma.controlRequirement.findMany({ + where: { + type: client_2.RequirementType.policy, + }, }); - if (!policy) { - console.log( - ` āš ļø Policy ${policyId} not found for requirement ${requirement.id}, skipping` - ); - continue; + console.log(`šŸ”„ Processing ${policyRequirements.length} policy requirements`); + for (const requirement of policyRequirements) { + // Get the controls file for this requirement to extract the policyId + const control = await prisma.control.findUnique({ + where: { id: requirement.controlId }, + include: { frameworkCategory: true }, + }); + if (!control) { + console.log(` āš ļø Control not found for requirement ${requirement.id}, skipping`); + continue; + } + if (!control.frameworkCategory) { + console.log(` āš ļø Framework category not found for control ${control.id}, skipping`); + continue; + } + // Get the framework ID from the category + const frameworkId = control.frameworkCategory.frameworkId; + // Get the controls data from the file + const controlsFile = (0, node_path_1.join)(__dirname, `../../data/controls/${frameworkId}.json`); + const controlsData = JSON.parse(node_fs_2.default.readFileSync(controlsFile, "utf8")); + // Find the requirement in the control data + const controlData = controlsData[control.code]; + if (!controlData) { + console.log(` āš ļø Control data not found for ${control.code} in framework ${frameworkId}, skipping`); + continue; + } + const reqData = controlData.requirements.find((req) => req.id === requirement.id); + if (!reqData) { + console.log(` āš ļø Requirement data not found for ${requirement.id} in control ${control.code}, skipping`); + continue; + } + // Get the policyId from the requirement data + const policyId = reqData.policyId; + if (!policyId) { + console.log(` āš ļø No policyId found for requirement ${requirement.id}, skipping`); + continue; + } + // Verify the policy exists + const policy = await prisma.policy.findUnique({ + where: { id: policyId }, + }); + if (!policy) { + console.log(` āš ļø Policy ${policyId} not found for requirement ${requirement.id}, skipping`); + continue; + } + console.log(` ā³ Linking requirement ${requirement.id} to policy ${policyId}...`); + // Update the control requirement to link to the policy + await prisma.controlRequirement.update({ + where: { + id: requirement.id, + }, + data: { + policyId: policyId, + name: policy.name, + description: policy.description || "", + frequency: policy.frequency, + department: policy.department ?? client_1.Departments.none, + }, + }); + console.log(` āœ… Requirement ${requirement.id} linked to policy ${policyId}`); } - console.log( - ` ā³ Linking requirement ${requirement.id} to policy ${policyId}...` - ); - // Update the control requirement to link to the policy - await prisma.controlRequirement.update({ - where: { - id: requirement.id, - }, - data: { - policyId: policyId, - name: policy.name, - description: policy.description || "", - frequency: policy.frequency, - department: policy.department ?? client_1.Departments.none, - }, - }); - console.log( - ` āœ… Requirement ${requirement.id} linked to policy ${policyId}` - ); - } } async function seedTrainingVideos() { - for (const video of data_1.trainingVideos) { - await prisma.portalTrainingVideos.upsert({ - where: { id: video.id }, - update: { - title: video.title, - description: video.description, - videoUrl: video.url, - youtubeId: video.youtubeId, - }, - create: { - id: video.id, - title: video.title, - description: video.description, - videoUrl: video.url, - youtubeId: video.youtubeId, - }, - }); - } + console.log(`šŸ”„ Seeding ${data_1.trainingVideos.length} training videos...`); + for (const video of data_1.trainingVideos) { + console.log(` ā³ Processing video: ${video.title}...`); + await prisma.portalTrainingVideos.upsert({ + where: { id: video.id }, + update: { + title: video.title, + description: video.description, + videoUrl: video.url, + youtubeId: video.youtubeId, + }, + create: { + id: video.id, + title: video.title, + description: video.description, + videoUrl: video.url, + youtubeId: video.youtubeId, + }, + }); + console.log(` āœ… Video ${video.title} processed`); + } + console.log("āœ… Training videos seeded"); }