diff --git a/apps/app/src/actions/framework/select-frameworks-action.ts b/apps/app/src/actions/framework/select-frameworks-action.ts index fdb076e..026f65d 100644 --- a/apps/app/src/actions/framework/select-frameworks-action.ts +++ b/apps/app/src/actions/framework/select-frameworks-action.ts @@ -149,6 +149,7 @@ const createOrganizationPolicy = async (user: User, frameworkIds: string[]) => { policyId: policy.id, status: "draft", content: policy.content as InputJsonValue[], + frequency: policy.frequency, })), }); @@ -265,6 +266,7 @@ const createOrganizationEvidence = async (user: User) => { evidenceId: evidence.id, name: evidence.name, description: evidence.description, + frequency: evidence.frequency, })), }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts index 24561bc..48775b3 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Actions/getOrganizationEvidenceTasks.ts @@ -1,13 +1,33 @@ "use server"; import { authActionClient } from "@/actions/safe-action"; -import { db } from "@bubba/db"; +import { db, Frequency } from "@bubba/db"; +import type { Prisma, OrganizationEvidence } from "@bubba/db"; import { z } from "zod"; +// Define the response types for better type safety +export interface PaginationMetadata { + page: number; + pageSize: number; + totalCount: number; + totalPages: number; + hasNextPage: boolean; + hasPreviousPage: boolean; +} + +export interface EvidenceTasksData { + data: OrganizationEvidence[]; + pagination: PaginationMetadata; +} + export const getOrganizationEvidenceTasks = authActionClient .schema( z.object({ search: z.string().optional().nullable(), + status: z.enum(["published", "draft"]).optional().nullable(), + frequency: z.nativeEnum(Frequency).optional().nullable(), + page: z.number().int().positive().optional().default(1), + pageSize: z.number().int().positive().optional().default(10), }) ) .metadata({ @@ -19,7 +39,7 @@ export const getOrganizationEvidenceTasks = authActionClient }) .action(async ({ ctx, parsedInput }) => { const { user } = ctx; - const { search } = parsedInput; + const { search, status, frequency, page, pageSize } = parsedInput; if (!user.organizationId) { return { @@ -29,44 +49,78 @@ export const getOrganizationEvidenceTasks = authActionClient } try { - const evidenceTasks = await db.organizationEvidence.findMany({ - where: { - organizationId: user.organizationId, - ...(search - ? { - OR: [ - { - name: { - contains: search, - mode: "insensitive", - }, + // Create the where clause for both count and data queries + const whereClause: Prisma.OrganizationEvidenceWhereInput = { + organizationId: user.organizationId, + // Status filter + ...(status === "published" ? { published: true } : {}), + ...(status === "draft" ? { published: false } : {}), + // Frequency filter + ...(frequency ? { frequency } : {}), + // Search filter + ...(search + ? { + OR: [ + { + name: { + contains: search, + mode: "insensitive" as Prisma.QueryMode, }, - { - description: { - contains: search, - mode: "insensitive", - }, + }, + { + description: { + contains: search, + mode: "insensitive" as Prisma.QueryMode, }, - { - evidence: { - name: { - contains: search, - mode: "insensitive", - }, + }, + { + evidence: { + name: { + contains: search, + mode: "insensitive" as Prisma.QueryMode, }, }, - ], - } - : {}), - }, + }, + ], + } + : {}), + }; + + // Get total count for pagination + const totalCount = await db.organizationEvidence.count({ + where: whereClause, + }); + + // Calculate pagination values + const skip = (page - 1) * pageSize; + const totalPages = Math.ceil(totalCount / pageSize); + + // Get paginated data + const evidenceTasks = await db.organizationEvidence.findMany({ + where: whereClause, include: { evidence: true, }, + skip, + take: pageSize, + orderBy: { + updatedAt: "desc", // Most recently updated first + }, }); return { success: true, - data: evidenceTasks, + data: { + data: evidenceTasks, + pagination: { + page, + pageSize, + totalCount, + totalPages, + hasNextPage: page < totalPages, + hasPreviousPage: page > 1, + }, + }, }; } catch (error) { console.error("Error fetching evidence tasks:", error); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx index c50383f..7d9bea1 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/EvidenceList.tsx @@ -4,44 +4,293 @@ import { useOrganizationEvidenceTasks } from "../hooks/useEvidenceTasks"; import { DataTable } from "./data-table/data-table"; import { Input } from "@bubba/ui/input"; import { useQueryState } from "nuqs"; -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { debounce } from "lodash"; import { useI18n } from "@/locales/client"; import { SkeletonTable } from "./SkeletonTable"; +import { + CheckCircle2, + Filter, + XCircle, + ChevronLeft, + ChevronRight, +} from "lucide-react"; +import { Button } from "@bubba/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + DropdownMenuCheckboxItem, +} from "@bubba/ui/dropdown-menu"; +import { Badge } from "@bubba/ui/badge"; +import type { Frequency } from "@bubba/db"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@bubba/ui/select"; +import type { EvidenceTaskRow } from "./data-table/types"; export const EvidenceList = () => { const t = useI18n(); const [search, setSearch] = useQueryState("search"); + const [status, setStatus] = useQueryState("status"); + const [frequency, setFrequency] = useQueryState("frequency"); + const [page, setPage] = useQueryState("page", { defaultValue: "1" }); + const [pageSize, setPageSize] = useQueryState("pageSize", { + defaultValue: "10", + }); + + const currentPage = Number.parseInt(page, 10); + const currentPageSize = Number.parseInt(pageSize, 10); + const { data: evidenceTasks, + pagination, isLoading, error, - } = useOrganizationEvidenceTasks({ search }); + } = useOrganizationEvidenceTasks({ + search, + status: status as "published" | "draft" | null, + frequency: frequency as Frequency | null, + page: currentPage, + pageSize: currentPageSize, + }); const handleSearch = useCallback( debounce((value: string) => { setSearch(value || null); + // Reset to first page when searching + setPage("1"); }, 300), - [setSearch] + [setSearch, setPage] ); - if (error) return
Error loading evidence tasks
; + console.log({ evidenceTasks, error }); + + // Get unique frequencies for the dropdown + const frequencies = useMemo(() => { + if (!evidenceTasks) return []; + + const uniqueFrequencies = new Set(); + for (const task of evidenceTasks) { + if (task.frequency) { + uniqueFrequencies.add(task.frequency); + } + } + + return Array.from(uniqueFrequencies).sort(); + }, [evidenceTasks]); + + // Clear all filters + const clearFilters = () => { + setStatus(null); + setFrequency(null); + // Reset to first page when clearing filters + setPage("1"); + }; + + // Handle page change + const handlePageChange = (newPage: number) => { + setPage(newPage.toString()); + }; + + // Handle page size change + const handlePageSizeChange = (newSize: string) => { + setPageSize(newSize); + // Reset to first page when changing page size + setPage("1"); + }; + + // Check if any filters are active + const hasActiveFilters = status !== null || frequency !== null; + + if (error) return
Error: {error.message}
; if (!evidenceTasks && !isLoading) return null; + // Convert the data to the expected format for the DataTable + const tableData: EvidenceTaskRow[] = + evidenceTasks?.map((task) => ({ + ...task, + evidence: { + name: task.name, + }, + })) || []; + return (
-

Review and upload evidence

-
+

Evidence Tasks

+
handleSearch(e.target.value)} defaultValue={search || ""} className="max-w-sm" /> + + + + + + + Filter by Status + { + setStatus(status === "published" ? null : "published"); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + Published +
+
+ { + setStatus(status === "draft" ? null : "draft"); + setPage("1"); // Reset to first page when filtering + }} + > +
+ + Draft +
+
+ + + + Filter by Frequency + {frequencies.map((freq) => ( + { + setFrequency(frequency === freq ? null : freq); + setPage("1"); // Reset to first page when filtering + }} + > + {freq} + + ))} + + {hasActiveFilters && ( + <> + + + + )} +
+
+ + {/* Active filter badges */} + {status && ( + { + setStatus(null); + setPage("1"); // Reset to first page when removing filter + }} + > + Status: {status === "published" ? "Published" : "Draft"} + + + )} + + {frequency && ( + { + setFrequency(null); + setPage("1"); // Reset to first page when removing filter + }} + > + Frequency: {frequency} + + + )}
- {isLoading ? : } + + {isLoading ? ( + + ) : ( + <> + + + {/* Pagination UI */} + {pagination && ( +
+
+ Showing {(currentPage - 1) * currentPageSize + 1} to{" "} + {Math.min(currentPage * currentPageSize, pagination.totalCount)}{" "} + of {pagination.totalCount} items +
+ +
+ + +
+ +
+ Page {currentPage} of {pagination.totalPages} +
+ +
+
+
+ )} + + )}
); }; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx index 3dc3138..2611e80 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/columns.tsx @@ -9,59 +9,24 @@ import { TooltipTrigger, } from "@bubba/ui/tooltip"; import type { EvidenceTaskRow } from "./types"; +import { calculateNextReview } from "@/lib/utils/calculate-next-review"; export const columns: ColumnDef[] = [ { id: "name", accessorKey: "name", header: "Name", - size: 200, - cell: ({ row }) => ( - - - -
{row.original.name}
-
- {row.original.name} -
-
- ), - }, - { - id: "description", - accessorKey: "description", - header: "Description", - size: 300, - cell: ({ row }) => { - const description = row.original.description; - if (!description) return null; - - return ( - - - -
{description}
-
- {description} -
-
- ); - }, - }, - { - id: "evidence", - accessorKey: "evidence.name", - header: "Evidence Type", - size: 150, + enableResizing: true, + enableSorting: true, + size: 100, + minSize: 200, cell: ({ row }) => ( - -
- {row.original.evidence.name} -
+ +
{row.original.name}
- {row.original.evidence.name} + {row.original.description}
), @@ -70,13 +35,16 @@ export const columns: ColumnDef[] = [ id: "status", accessorKey: "published", header: "Status", - size: 100, + enableResizing: true, + enableSorting: true, + size: 150, + minSize: 120, cell: ({ row }) => { const isPublished = row.original.published; const label = isPublished ? "Published" : "Draft"; return ( -
+
{isPublished ? ( ) : ( @@ -93,4 +61,49 @@ export const columns: ColumnDef[] = [ ); }, }, + { + id: "frequency", + accessorKey: "frequency", + header: "Frequency", + size: 150, + enableResizing: true, + minSize: 130, + enableSorting: true, + cell: ({ row }) => { + const frequency = row.original.frequency; + if (!frequency) return null; + + return
{frequency}
; + }, + }, + { + id: "nextReviewDate", + accessorKey: "nextReviewDate", + header: "Next Review Date", + size: 150, + enableResizing: true, + minSize: 180, + enableSorting: true, + cell: ({ row }) => { + if (row.original.lastPublishedAt === null) { + return
ASAP
; + } + + const reviewInfo = calculateNextReview( + row.original.lastPublishedAt, + row.original.frequency + ); + + if (!reviewInfo) return null; + + return ( +
+ {reviewInfo.daysUntil} days ( + {reviewInfo.nextReviewDate.toLocaleDateString()}) +
+ ); + }, + }, ]; diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table-header.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table-header.tsx index 1be2eda..f454901 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table-header.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table-header.tsx @@ -2,18 +2,62 @@ import type { Table } from "@tanstack/react-table"; import { TableHead, TableHeader, TableRow } from "@bubba/ui/table"; +import { flexRender } from "@tanstack/react-table"; import type { EvidenceTaskRow } from "./types"; +import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"; +import { cn } from "@bubba/ui/cn"; -export function DataTableHeader({ table }: { table: Table }) { +interface DataTableHeaderProps { + table: Table; +} + +export function DataTableHeader({ table }: DataTableHeaderProps) { return ( - - {table.getAllColumns().map((column) => ( - - {column.columnDef.header as string} - - ))} - + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext() + )} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? + (header.column.getCanSort() && ( + + ))} +
+ )} +
+ + ))} + + ))} ); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table.tsx index 57fc3a1..582f6ca 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/Components/data-table/data-table.tsx @@ -1,60 +1,94 @@ "use client"; import { + type Column, flexRender, getCoreRowModel, useReactTable, + getSortedRowModel, + type SortingState, } from "@tanstack/react-table"; import { Table, TableBody, TableCell, TableRow } from "@bubba/ui/table"; import { columns } from "./columns"; import { DataTableHeader } from "./data-table-header"; import type { EvidenceTaskRow } from "./types"; import { useRouter } from "next/navigation"; +import { useState } from "react"; export function DataTable({ data }: { data: EvidenceTaskRow[] }) { const router = useRouter(); + const [sorting, setSorting] = useState([ + { + id: "name", + desc: false, + }, + ]); + const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableColumnResizing: true, + columnResizeMode: "onChange", + state: { + sorting, + }, + onSortingChange: setSorting, }); return (
-
- - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - router.push(`/evidence/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} +
+
+
+ + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + router.push(`/evidence/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+
+ + ))} + + )) + ) : ( + + + No evidence tasks found. + - )) - ) : ( - - - No evidence tasks found. - - - )} - -
+ )} + + +
); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/publishEvidence.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/publishEvidence.ts index 604efe2..a9a58f2 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/publishEvidence.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Actions/publishEvidence.ts @@ -60,6 +60,7 @@ export const publishEvidence = authActionClient where: { id }, data: { published: true, + lastPublishedAt: new Date(), }, }); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx index 19f3088..b6f5e97 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/EvidenceDetails.tsx @@ -4,11 +4,12 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@bubba/ui/button"; import { ArrowLeft, CheckCircle2, XCircle } from "lucide-react"; -import { Card, CardContent, CardHeader } from "@bubba/ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card"; import { Skeleton } from "@bubba/ui/skeleton"; import { useOrganizationEvidence } from "../hooks/useOrganizationEvidence"; import { FileSection } from "./FileSection"; import { UrlSection } from "./UrlSection"; +import { ReviewDateCard } from "./ReviewDateCard"; import { publishEvidence } from "../Actions/publishEvidence"; import { useAction } from "next-safe-action/hooks"; import { toast } from "sonner"; @@ -84,6 +85,13 @@ export function EvidenceDetails({ id }: EvidenceDetailsProps) { )}
+ {evidence.frequency && ( + + )} +
{evidence.evidence.name}
diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileCard.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileCard.tsx index 84aaada..8ecd453 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileCard.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileCard.tsx @@ -51,17 +51,31 @@ export function FileCard({ const isPdf = /\.pdf$/i.test(fileName); return ( - - + + - @@ -100,46 +114,58 @@ export function FileCard({ )} + + -

- {fileName} -

+
-

{fileName}

+

Open file

-
- - + - - - + + + + + + + + +

Delete file

+
+
+
Delete File @@ -150,10 +176,7 @@ export function FileCard({ Cancel - onDelete(url)} - className="bg-destructive hover:bg-destructive/90" - > + onDelete(url)}> Delete diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx index a3f7095..eff5bf5 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/FileSection.tsx @@ -6,6 +6,8 @@ import { useFileUpload } from "../hooks/useFileUpload"; import { useFileDelete } from "../hooks/useFileDelete"; import { useFilePreview } from "../hooks/useFilePreview"; import { FileCard } from "./FileCard"; +import { Card, CardContent } from "@bubba/ui/card"; +import { Plus } from "lucide-react"; interface FileSectionProps { evidenceId: string; @@ -42,6 +44,7 @@ export function FileSection({ Record >({}); const [openDialogId, setOpenDialogId] = useState(null); + const [showUploadDropzone, setShowUploadDropzone] = useState(false); const handlePreviewClick = useCallback( async (fileUrl: string) => { @@ -77,37 +80,65 @@ export function FileSection({
- - - {fileUrls.length > 0 && ( -
- {fileUrls.map((url) => { - const previewState = previewStates[url] || { - url: null, - isLoading: false, - }; - - return ( - { - if (open) { - setOpenDialogId(url); - handlePreviewClick(url); - } else { - setOpenDialogId(null); - } - }} - onPreviewClick={handlePreviewClick} - onDelete={handleDelete} - /> - ); - })} + {/* Only show the full dropzone when no files exist or when explicitly shown */} + {(fileUrls.length === 0 || showUploadDropzone) && ( +
+ { + handleFileUpload(file); + setShowUploadDropzone(false); + }} + isUploading={isUploading} + />
)} + +
+ {/* Add Files Card - Always first in the grid */} + {!showUploadDropzone && fileUrls.length > 0 && ( + setShowUploadDropzone(true)} + > + +
+ +
+

Add Files

+

+ Upload additional evidence files +

+
+
+ )} + + {/* File Cards */} + {fileUrls.map((url) => { + const previewState = previewStates[url] || { + url: null, + isLoading: false, + }; + + return ( + { + if (open) { + setOpenDialogId(url); + handlePreviewClick(url); + } else { + setOpenDialogId(null); + } + }} + onPreviewClick={handlePreviewClick} + onDelete={handleDelete} + /> + ); + })} +
); } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/ReviewDateCard.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/ReviewDateCard.tsx new file mode 100644 index 0000000..a8f9cf1 --- /dev/null +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/ReviewDateCard.tsx @@ -0,0 +1,42 @@ +"use client"; + +import type { Frequency } from "@bubba/db"; +import { Card, CardHeader, CardTitle, CardContent } from "@bubba/ui/card"; +import { calculateNextReview } from "@/lib/utils/calculate-next-review"; + +export function ReviewDateCard({ + lastPublishedAt, + frequency, +}: { + lastPublishedAt: Date | null; + frequency: Frequency | null; +}) { + const reviewInfo = calculateNextReview(lastPublishedAt, frequency); + + if (!reviewInfo) { + return ( + + + Next Review Date + + +

ASAP

+
+
+ ); + } + + return ( + + + Next Review Date + + +
+ {reviewInfo.daysUntil} days ( + {reviewInfo.nextReviewDate.toLocaleDateString()}) +
+
+
+ ); +} diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/UrlSection.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/UrlSection.tsx index 64a2514..2ccf6ec 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/UrlSection.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/[id]/Components/UrlSection.tsx @@ -2,8 +2,16 @@ import { Button } from "@bubba/ui/button"; import { Input } from "@bubba/ui/input"; -import { Plus, Trash, Save } from "lucide-react"; +import { Card, CardContent } from "@bubba/ui/card"; +import { Plus, Trash, Save, Link, ExternalLink, Copy } from "lucide-react"; import { useUrlManagement } from "../hooks/useUrlManagement"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@bubba/ui/tooltip"; +import { Badge } from "@bubba/ui/badge"; interface UrlSectionProps { evidenceId: string; @@ -29,48 +37,51 @@ export function UrlSection({ onSuccess, }); + // Function to get domain name from URL for display + const getDomainName = (url: string) => { + try { + const domain = new URL(url).hostname; + return domain.replace(/^www\./, ""); + } catch (e) { + return url; + } + }; + + // Copy URL to clipboard + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + }; + return ( -
+
-

Additional Links

+
+

Additional Links

+ {draftUrls.length === 0 && ( + + )} +
{additionalUrls.length} link{additionalUrls.length !== 1 ? "s" : ""}{" "} added
-
-
- {/* Existing URLs */} - {additionalUrls.map((url) => ( -
- - {url} - - -
- ))} - - {/* Draft URLs */} + {/* Draft URL Input Section */} + {draftUrls.length > 0 && ( +
{draftUrls.map((draft) => (
handleRemoveDraft(draft.id)} - className="h-8 w-8 hover:text-destructive" + className="h-9 w-9 hover:text-destructive" >
))} - {/* Action Buttons */} -
+
+ - {draftUrls.length > 0 && ( - - )}
+ )} + +
+ {/* URL List */} + {additionalUrls.length > 0 && ( +
+ {additionalUrls.map((url) => { + const domain = getDomainName(url); + + return ( +
+
+ +
+ +
+
+

{domain}

+ + URL + +
+

+ {url} +

+
+ +
+ + + + + + +

Copy link

+
+
+
+ + + + + + + +

Open link

+
+
+
+ + + + + + + +

Delete link

+
+
+
+
+
+ ); + })} +
+ )}
); diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts index 43b3e92..6495fad 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/hooks/useEvidenceTasks.ts @@ -2,36 +2,66 @@ import useSWR from "swr"; import { getOrganizationEvidenceTasks } from "../Actions/getOrganizationEvidenceTasks"; +import type { PaginationMetadata } from "../Actions/getOrganizationEvidenceTasks"; +import type { Frequency, OrganizationEvidence } from "@bubba/db"; +// Define the props interface with clear types interface UseOrganizationEvidenceTasksProps { search?: string | null; + status?: "published" | "draft" | null; + frequency?: Frequency | null; + page?: number; + pageSize?: number; } -async function fetchEvidenceTasks({ - search, -}: UseOrganizationEvidenceTasksProps) { - const result = await getOrganizationEvidenceTasks({ search }); +// Define the hook result type +interface UseOrganizationEvidenceTasksResult { + data: OrganizationEvidence[] | undefined; + pagination: PaginationMetadata | undefined; + isLoading: boolean; + error: Error | undefined; + mutate: () => void; +} + +async function fetchEvidenceTasks(props: UseOrganizationEvidenceTasksProps) { + const result = await getOrganizationEvidenceTasks(props); + + if (!result) { + throw new Error("No result received"); + } - if (!result || "error" in result) { + if (result.serverError) { + throw new Error(result.serverError); + } + + if (result.validationErrors) { throw new Error( - typeof result?.error === "string" - ? result.error - : "Failed to fetch evidence tasks" + result.validationErrors._errors?.join(", ") ?? "Unknown error" ); } return result.data?.data; } -export function useOrganizationEvidenceTasks({ - search, -}: UseOrganizationEvidenceTasksProps = {}) { - return useSWR( - ["organization-evidence-tasks", search], - () => fetchEvidenceTasks({ search }), +export function useOrganizationEvidenceTasks( + props: UseOrganizationEvidenceTasksProps = {} +): UseOrganizationEvidenceTasksResult { + const { search, status, frequency, page = 1, pageSize = 10 } = props; + + const { data, error, isLoading, mutate } = useSWR( + ["organization-evidence-tasks", search, status, frequency, page, pageSize], + () => fetchEvidenceTasks({ search, status, frequency, page, pageSize }), { revalidateOnFocus: false, revalidateOnReconnect: false, } ); + + return { + data: data?.data ?? [], + pagination: data?.pagination, + isLoading, + error, + mutate, + }; } diff --git a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/layout.tsx b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/layout.tsx index 01ac815..a5cfa0b 100644 --- a/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/layout.tsx +++ b/apps/app/src/app/[locale]/(app)/(dashboard)/evidence/layout.tsx @@ -11,9 +11,7 @@ export default async function Layout({ return (
- - -
{children}
+
{children}
); } diff --git a/apps/app/src/lib/utils/calculate-next-review.ts b/apps/app/src/lib/utils/calculate-next-review.ts new file mode 100644 index 0000000..35fa830 --- /dev/null +++ b/apps/app/src/lib/utils/calculate-next-review.ts @@ -0,0 +1,42 @@ +import type { Frequency } from "@bubba/db"; + +interface ReviewInfo { + nextReviewDate: Date; + daysUntil: number; + isUrgent: boolean; +} + +export function calculateNextReview( + lastPublishedAt: Date | null, + frequency: Frequency | null, + urgentThresholdDays = 7 +): ReviewInfo | null { + if (!frequency || !lastPublishedAt) return null; + + const baseDate = new Date(lastPublishedAt); + const nextReviewDate = new Date(baseDate); + + switch (frequency) { + case "monthly": + nextReviewDate.setMonth(nextReviewDate.getMonth() + 1); + break; + case "quarterly": + nextReviewDate.setMonth(nextReviewDate.getMonth() + 3); + break; + case "yearly": + nextReviewDate.setFullYear(nextReviewDate.getFullYear() + 1); + break; + default: + return null; + } + + const daysUntil = Math.ceil( + (nextReviewDate.getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24) + ); + + return { + nextReviewDate, + daysUntil, + isUrgent: daysUntil < urgentThresholdDays, + }; +} diff --git a/packages/data/controls/soc2.json b/packages/data/controls/soc2.json index c18cbdd..daebea6 100644 --- a/packages/data/controls/soc2.json +++ b/packages/data/controls/soc2.json @@ -11,7 +11,8 @@ "type": "policy", "name": "Corporate Governance Policy", "description": "Reference to the Corporate Governance Policy that defines board oversight responsibilities, roles, review frequency, and reporting requirements.", - "policyId": "corporate_governance" + "policyId": "corporate_governance", + "frequency": "yearly" }, { "id": "CC1.1-procedure", @@ -23,7 +24,8 @@ "id": "CC1.1-evidence", "type": "evidence", "name": "Board Meeting Documentation", - "description": "Minutes of board meetings and oversight reports demonstrating active review of internal controls." + "description": "Minutes of board meetings and oversight reports demonstrating active review of internal controls.", + "frequency": "quarterly" } ] }, @@ -39,7 +41,8 @@ "type": "policy", "name": "Corporate Governance Policy", "description": "Reference to the Corporate Governance Policy that outlines management responsibilities and the organizational structure for effective oversight.", - "policyId": "corporate_governance" + "policyId": "corporate_governance", + "frequency": "yearly" }, { "id": "CC1.2-procedure", @@ -57,7 +60,8 @@ "id": "CC1.2-evidence", "type": "evidence", "name": "Management Structure Documentation", - "description": "Organizational charts, management meeting minutes, and training records." + "description": "Organizational charts, management meeting minutes, and training records.", + "frequency": "yearly" } ] }, @@ -73,7 +77,8 @@ "type": "policy", "name": "Human Resources Policy", "description": "Reference to the Human Resources Policy that defines recruitment, retention, and competency requirements.", - "policyId": "human_resources" + "policyId": "human_resources", + "frequency": "yearly" }, { "id": "CC1.3-procedure", @@ -85,7 +90,8 @@ "id": "CC1.3-evidence", "type": "evidence", "name": "HR Documentation", - "description": "HR records, training logs, and performance evaluations." + "description": "HR records, training logs, and performance evaluations.", + "frequency": "yearly" } ] }, @@ -101,7 +107,8 @@ "type": "policy", "name": "Human Resources Policy", "description": "Reference to the Human Resources Policy that outlines roles, responsibilities, and disciplinary measures.", - "policyId": "human_resources" + "policyId": "human_resources", + "frequency": "yearly" }, { "id": "CC1.4-procedure", @@ -119,7 +126,8 @@ "id": "CC1.4-evidence", "type": "evidence", "name": "Personnel Compliance Documentation", - "description": "Employee acknowledgment forms, training records, and disciplinary documentation." + "description": "Employee acknowledgment forms, training records, and disciplinary documentation.", + "frequency": "yearly" } ] }, @@ -135,7 +143,8 @@ "type": "policy", "name": "Corporate Governance Policy", "description": "Reference to the Corporate Governance Policy (or a dedicated Code of Conduct within it) that defines ethical behavior and compliance expectations.", - "policyId": "corporate_governance" + "policyId": "corporate_governance", + "frequency": "yearly" }, { "id": "CC1.5-procedure", @@ -153,7 +162,8 @@ "id": "CC1.5-evidence", "type": "evidence", "name": "Ethics Compliance Documentation", - "description": "Signed acknowledgment forms, training completion records, and records of investigations or disciplinary actions." + "description": "Signed acknowledgment forms, training completion records, and records of investigations or disciplinary actions.", + "frequency": "yearly" } ] }, @@ -169,7 +179,8 @@ "type": "policy", "name": "Information Security Policy", "description": "Reference to the Information Security Policy that outlines data accuracy, completeness, and timeliness requirements.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "CC2.1-procedure", @@ -181,7 +192,8 @@ "id": "CC2.1-evidence", "type": "evidence", "name": "Data Quality Documentation", - "description": "Data quality reports, audit logs, and records of corrective actions." + "description": "Data quality reports, audit logs, and records of corrective actions.", + "frequency": "quarterly" } ] }, @@ -197,7 +209,8 @@ "type": "policy", "name": "Corporate Governance Policy", "description": "Reference to the Corporate Governance Policy that includes guidelines for internal communications of control objectives.", - "policyId": "corporate_governance" + "policyId": "corporate_governance", + "frequency": "yearly" }, { "id": "CC2.2-procedure", @@ -209,7 +222,8 @@ "id": "CC2.2-evidence", "type": "evidence", "name": "Communication Records", - "description": "Communication logs, email distributions, and meeting minutes." + "description": "Communication logs, email distributions, and meeting minutes.", + "frequency": "quarterly" } ] }, @@ -225,7 +239,8 @@ "type": "policy", "name": "Corporate Governance Policy", "description": "Reference to the Corporate Governance Policy that outlines external communication guidelines for control-related matters.", - "policyId": "corporate_governance" + "policyId": "corporate_governance", + "frequency": "yearly" }, { "id": "CC2.3-procedure", @@ -237,7 +252,8 @@ "id": "CC2.3-evidence", "type": "evidence", "name": "External Communication Records", - "description": "Records of external communications, press releases, and stakeholder correspondence." + "description": "Records of external communications, press releases, and stakeholder correspondence.", + "frequency": "yearly" } ] }, @@ -253,7 +269,8 @@ "type": "policy", "name": "Risk Management Policy", "description": "Reference to the Risk Management Policy that defines methodologies, frequency, and scope for risk assessments.", - "policyId": "risk_management" + "policyId": "risk_management", + "frequency": "yearly" }, { "id": "CC3.1-procedure", @@ -265,7 +282,8 @@ "id": "CC3.1-evidence", "type": "evidence", "name": "Risk Assessment Documentation", - "description": "Risk assessment reports, risk registers, and management review minutes." + "description": "Risk assessment reports, risk registers, and management review minutes.", + "frequency": "yearly" } ] }, @@ -281,7 +299,8 @@ "type": "policy", "name": "Risk Management Policy", "description": "Reference to the Risk Management Policy requiring systematic identification of risks across all business areas.", - "policyId": "risk_management" + "policyId": "risk_management", + "frequency": "yearly" }, { "id": "CC3.2-procedure", @@ -293,7 +312,8 @@ "id": "CC3.2-evidence", "type": "evidence", "name": "Risk Identification Records", - "description": "Risk register entries, workshop records, and risk analysis documentation." + "description": "Risk register entries, workshop records, and risk analysis documentation.", + "frequency": "yearly" } ] }, @@ -309,7 +329,8 @@ "type": "policy", "name": "Risk Management Policy", "description": "Reference to the Risk Management Policy with provisions for assessing and mitigating fraud risks.", - "policyId": "risk_management" + "policyId": "risk_management", + "frequency": "yearly" }, { "id": "CC3.3-procedure", @@ -321,7 +342,8 @@ "id": "CC3.3-evidence", "type": "evidence", "name": "Fraud Risk Documentation", - "description": "Fraud risk assessment reports, internal audit findings, and remediation tracking records." + "description": "Fraud risk assessment reports, internal audit findings, and remediation tracking records.", + "frequency": "yearly" } ] }, @@ -337,7 +359,8 @@ "type": "policy", "name": "Change Management Policy", "description": "Reference to the Change Management Policy that outlines how risks associated with changes are evaluated.", - "policyId": "change_management" + "policyId": "change_management", + "frequency": "yearly" }, { "id": "CC3.4-procedure", @@ -349,7 +372,8 @@ "id": "CC3.4-evidence", "type": "evidence", "name": "Change Risk Documentation", - "description": "Change impact assessments, risk logs, and management approval records." + "description": "Change impact assessments, risk logs, and management approval records.", + "frequency": "yearly" } ] }, @@ -365,7 +389,8 @@ "type": "policy", "name": "Information Security Policy", "description": "Reference to the Information Security Policy that outlines monitoring requirements for internal controls.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "CC4.1-procedure", @@ -377,7 +402,8 @@ "id": "CC4.1-evidence", "type": "evidence", "name": "Control Testing Documentation", - "description": "Internal audit reports, control testing records, and management review documentation." + "description": "Internal audit reports, control testing records, and management review documentation.", + "frequency": "quarterly" } ] }, @@ -393,7 +419,8 @@ "type": "policy", "name": "Risk Management Policy", "description": "Reference to the Risk Management Policy that establishes a framework for identifying, reporting, and remediating control deficiencies.", - "policyId": "risk_management" + "policyId": "risk_management", + "frequency": "yearly" }, { "id": "CC4.2-procedure", @@ -405,7 +432,8 @@ "id": "CC4.2-evidence", "type": "evidence", "name": "Deficiency Management Records", - "description": "Deficiency logs, remediation plans, and follow-up audit reports." + "description": "Deficiency logs, remediation plans, and follow-up audit reports.", + "frequency": "quarterly" } ] }, @@ -421,7 +449,8 @@ "type": "policy", "name": "Information Security Policy", "description": "Reference to the Information Security Policy that outlines criteria for the design and selection of controls.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "CC5.1-procedure", @@ -433,7 +462,8 @@ "id": "CC5.1-evidence", "type": "evidence", "name": "Control Implementation Records", - "description": "Control design documents, implementation records, and risk mitigation assessments." + "description": "Control design documents, implementation records, and risk mitigation assessments.", + "frequency": "yearly" } ] }, @@ -449,7 +479,8 @@ "type": "policy", "name": "Information Security Policy", "description": "Reference to the Information Security Policy that specifies baseline security configurations and technology management practices.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "CC5.2-procedure", @@ -461,7 +492,8 @@ "id": "CC5.2-evidence", "type": "evidence", "name": "Technology Control Records", - "description": "System configuration records, monitoring reports, and technology audit logs." + "description": "System configuration records, monitoring reports, and technology audit logs.", + "frequency": "quarterly" } ] }, @@ -477,7 +509,8 @@ "type": "policy", "name": "Corporate Governance Policy", "description": "Reference to the Corporate Governance Policy that outlines how policies are communicated, enforced, and monitored.", - "policyId": "corporate_governance" + "policyId": "corporate_governance", + "frequency": "yearly" }, { "id": "CC5.3-procedure", @@ -489,7 +522,8 @@ "id": "CC5.3-evidence", "type": "evidence", "name": "Policy Implementation Records", - "description": "Policy distribution records, training logs, and compliance monitoring reports." + "description": "Policy distribution records, training logs, and compliance monitoring reports.", + "frequency": "yearly" } ] }, @@ -505,7 +539,8 @@ "type": "policy", "name": "Access Control Policy", "description": "Reference to the Access Control Policy that defines controls for network and system access including segmentation and firewalls.", - "policyId": "access_control" + "policyId": "access_control", + "frequency": "yearly" }, { "id": "CC6.1-procedure", @@ -517,7 +552,8 @@ "id": "CC6.1-evidence", "type": "evidence", "name": "Access Control Records", - "description": "Access control configurations, firewall logs, and system access review reports." + "description": "Access control configurations, firewall logs, and system access review reports.", + "frequency": "quarterly" } ] }, @@ -533,7 +569,8 @@ "type": "policy", "name": "Access Control Policy", "description": "Reference to the Access Control Policy requiring strong authentication methods including passwords and multi-factor authentication.", - "policyId": "access_control" + "policyId": "access_control", + "frequency": "yearly" }, { "id": "CC6.2-procedure", @@ -545,7 +582,8 @@ "id": "CC6.2-evidence", "type": "evidence", "name": "Authentication Records", - "description": "Authentication logs, MFA configuration records, and user account management records." + "description": "Authentication logs, MFA configuration records, and user account management records.", + "frequency": "quarterly" } ] }, @@ -561,7 +599,8 @@ "type": "policy", "name": "Access Control Policy", "description": "Reference to the Access Control Policy detailing prompt revocation of user access upon termination or role change.", - "policyId": "access_control" + "policyId": "access_control", + "frequency": "yearly" }, { "id": "CC6.3-procedure", @@ -573,7 +612,8 @@ "id": "CC6.3-evidence", "type": "evidence", "name": "Access Removal Records", - "description": "User termination logs, de-provisioning records, and access review reports." + "description": "User termination logs, de-provisioning records, and access review reports.", + "frequency": "quarterly" } ] }, @@ -589,7 +629,8 @@ "type": "policy", "name": "Access Control Policy", "description": "Reference to the Access Control Policy mandating periodic reviews of user access rights and privileges.", - "policyId": "access_control" + "policyId": "access_control", + "frequency": "yearly" }, { "id": "CC6.4-procedure", @@ -601,7 +642,8 @@ "id": "CC6.4-evidence", "type": "evidence", "name": "Access Review Records", - "description": "Access review logs, user access reports, and management sign-off documentation." + "description": "Access review logs, user access reports, and management sign-off documentation.", + "frequency": "quarterly" } ] }, @@ -617,7 +659,8 @@ "type": "policy", "name": "Access Control Policy", "description": "Reference to the Access Control Policy that defines processes for creating, maintaining, and terminating system accounts.", - "policyId": "access_control" + "policyId": "access_control", + "frequency": "yearly" }, { "id": "CC6.5-procedure", @@ -629,7 +672,8 @@ "id": "CC6.5-evidence", "type": "evidence", "name": "Account Management Records", - "description": "Account provisioning logs, privileged account review reports, and system audit logs." + "description": "Account provisioning logs, privileged account review reports, and system audit logs.", + "frequency": "quarterly" } ] }, @@ -645,7 +689,8 @@ "type": "policy", "name": "Access Control Policy", "description": "Reference to the Access Control Policy that establishes requirements for securing facilities and restricting physical entry to sensitive areas.", - "policyId": "access_control" + "policyId": "access_control", + "frequency": "yearly" }, { "id": "CC6.6-procedure", @@ -657,7 +702,8 @@ "id": "CC6.6-evidence", "type": "evidence", "name": "Physical Access Records", - "description": "Facility access logs, visitor sign-in records, and security camera reports." + "description": "Facility access logs, visitor sign-in records, and security camera reports.", + "frequency": "quarterly" } ] }, @@ -673,7 +719,8 @@ "type": "policy", "name": "Change Management Policy", "description": "Reference to the Change Management Policy that governs modifications to information assets.", - "policyId": "change_management" + "policyId": "change_management", + "frequency": "yearly" }, { "id": "CC6.7-procedure", @@ -685,7 +732,8 @@ "id": "CC6.7-evidence", "type": "evidence", "name": "Change Management Records", - "description": "Change logs, approval records, and configuration management documentation." + "description": "Change logs, approval records, and configuration management documentation.", + "frequency": "yearly" } ] }, @@ -701,7 +749,8 @@ "type": "policy", "name": "Information Security Policy", "description": "Reference to the Information Security Policy that requires the use of antivirus, anti-malware, and threat detection solutions.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "CC6.8-procedure", @@ -713,7 +762,8 @@ "id": "CC6.8-evidence", "type": "evidence", "name": "Malware Prevention Records", - "description": "Antivirus logs, malware detection alerts, and remediation records." + "description": "Antivirus logs, malware detection alerts, and remediation records.", + "frequency": "quarterly" } ] }, @@ -729,7 +779,8 @@ "type": "policy", "name": "Information Security Policy", "description": "Reference to the Information Security Policy that outlines monitoring requirements for infrastructure and systems.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "CC7.1-procedure", @@ -741,7 +792,8 @@ "id": "CC7.1-evidence", "type": "evidence", "name": "Infrastructure Monitoring Records", - "description": "Monitoring tool reports, alert logs, and incident tracking records." + "description": "Monitoring tool reports, alert logs, and incident tracking records.", + "frequency": "quarterly" } ] }, @@ -757,7 +809,8 @@ "type": "policy", "name": "Incident Response Policy", "description": "Reference to the Incident Response Policy that defines roles, responsibilities, and procedures for addressing security events.", - "policyId": "incident_response" + "policyId": "incident_response", + "frequency": "yearly" }, { "id": "CC7.2-procedure", @@ -769,7 +822,8 @@ "id": "CC7.2-evidence", "type": "evidence", "name": "Incident Response Records", - "description": "Incident logs, response drill reports, and post-incident review documentation." + "description": "Incident logs, response drill reports, and post-incident review documentation.", + "frequency": "yearly" } ] }, @@ -785,7 +839,8 @@ "type": "policy", "name": "Business Continuity Policy", "description": "Reference to the Business Continuity & Disaster Recovery Policy that outlines strategies for restoring systems and data.", - "policyId": "business_continuity" + "policyId": "business_continuity", + "frequency": "yearly" }, { "id": "CC7.3-procedure", @@ -797,7 +852,8 @@ "id": "CC7.3-evidence", "type": "evidence", "name": "Recovery Records", - "description": "Recovery test results, restoration logs, and incident recovery reports." + "description": "Recovery test results, restoration logs, and incident recovery reports.", + "frequency": "yearly" } ] }, @@ -812,7 +868,8 @@ "id": "CC7.4-policy", "type": "policy", "description": "Reference to the Incident Response Policy that requires root cause analysis for security incidents.", - "policyId": "incident_response" + "policyId": "incident_response", + "frequency": "yearly" }, { "id": "CC7.4-procedure", @@ -823,7 +880,8 @@ "id": "CC7.4-evidence", "type": "evidence", "name": "Incident Analysis Records", - "description": "Incident analysis reports, lessons learned documentation, and remediation tracking records." + "description": "Incident analysis reports, lessons learned documentation, and remediation tracking records.", + "frequency": "yearly" } ] }, @@ -838,7 +896,8 @@ "id": "CC7.5-policy", "type": "policy", "description": "Reference to the Incident Response Policy that defines processes for internal and external incident notifications.", - "policyId": "incident_response" + "policyId": "incident_response", + "frequency": "yearly" }, { "id": "CC7.5-procedure", @@ -849,7 +908,8 @@ "id": "CC7.5-evidence", "type": "evidence", "name": "Incident Communication Records", - "description": "Communication logs, notifications, and records of stakeholder communications during incidents." + "description": "Communication logs, notifications, and records of stakeholder communications during incidents.", + "frequency": "yearly" } ] }, @@ -864,7 +924,8 @@ "id": "CC8.1-policy", "type": "policy", "description": "Reference to the Change Management Policy that establishes controls for reviewing, approving, and documenting changes.", - "policyId": "change_management" + "policyId": "change_management", + "frequency": "yearly" }, { "id": "CC8.1-procedure", @@ -875,7 +936,8 @@ "id": "CC8.1-evidence", "type": "evidence", "name": "Change Request Logs", - "description": "Change request logs, approval records, and post-change review reports." + "description": "Change request logs, approval records, and post-change review reports.", + "frequency": "yearly" } ] }, @@ -890,7 +952,8 @@ "id": "CC9.1-policy", "type": "policy", "description": "Reference to the Business Continuity & Disaster Recovery Policy that outlines recovery objectives, strategies, and responsibilities.", - "policyId": "business_continuity" + "policyId": "business_continuity", + "frequency": "yearly" }, { "id": "CC9.1-procedure", @@ -901,7 +964,8 @@ "id": "CC9.1-evidence", "type": "evidence", "name": "Business Continuity Plans", - "description": "Business continuity plans, BIA reports, and disaster recovery test results." + "description": "Business continuity plans, BIA reports, and disaster recovery test results.", + "frequency": "yearly" } ] }, @@ -916,7 +980,8 @@ "id": "CC9.2-policy", "type": "policy", "description": "Reference to the Vendor Risk Management Policy outlining criteria for vendor selection, risk assessment, and ongoing monitoring.", - "policyId": "vendor_risk_management" + "policyId": "vendor_risk_management", + "frequency": "yearly" }, { "id": "CC9.2-procedure", @@ -927,7 +992,8 @@ "id": "CC9.2-evidence", "type": "evidence", "name": "Vendor Risk Assessment Records", - "description": "Vendor risk assessment reports, due diligence records, and contract reviews." + "description": "Vendor risk assessment reports, due diligence records, and contract reviews.", + "frequency": "yearly" } ] }, @@ -942,7 +1008,8 @@ "id": "CC9.9-policy", "type": "policy", "description": "Reference to the Business Continuity & Disaster Recovery Policy that defines testing frequency, methodologies, and remediation procedures.", - "policyId": "business_continuity" + "policyId": "business_continuity", + "frequency": "yearly" }, { "id": "CC9.9-procedure", @@ -953,7 +1020,8 @@ "id": "CC9.9-evidence", "type": "evidence", "name": "Business Continuity and Disaster Recovery Testing Records", - "description": "Test reports, remediation logs, and updated BC/DR plans." + "description": "Test reports, remediation logs, and updated BC/DR plans.", + "frequency": "yearly" } ] }, @@ -968,7 +1036,8 @@ "id": "A1.1-policy", "type": "policy", "description": "Reference to the Availability Policy that outlines uptime, performance, and service level requirements.", - "policyId": "availability" + "policyId": "availability", + "frequency": "yearly" }, { "id": "A1.1-procedure", @@ -979,7 +1048,8 @@ "id": "A1.1-evidence", "type": "evidence", "name": "Uptime Reports", - "description": "Uptime reports, incident logs, and SLA monitoring records." + "description": "Uptime reports, incident logs, and SLA monitoring records.", + "frequency": "quarterly" } ] }, @@ -994,7 +1064,8 @@ "id": "A1.2-policy", "type": "policy", "description": "Reference to the Availability Policy defining procedures for monitoring, forecasting, and managing system capacity.", - "policyId": "availability" + "policyId": "availability", + "frequency": "yearly" }, { "id": "A1.2-procedure", @@ -1005,7 +1076,8 @@ "id": "A1.2-evidence", "type": "evidence", "name": "Capacity Reports", - "description": "Capacity reports, trend analysis, and resource utilization logs." + "description": "Capacity reports, trend analysis, and resource utilization logs.", + "frequency": "quarterly" } ] }, @@ -1020,7 +1092,8 @@ "id": "A1.3-policy", "type": "policy", "description": "Reference to the Business Continuity & Disaster Recovery Policy that outlines procedures to restore services after an outage.", - "policyId": "business_continuity" + "policyId": "business_continuity", + "frequency": "yearly" }, { "id": "A1.3-procedure", @@ -1031,7 +1104,8 @@ "id": "A1.3-evidence", "type": "evidence", "name": "Incident Recovery Records", - "description": "Incident recovery test results, post-incident reviews, and restoration logs." + "description": "Incident recovery test results, post-incident reviews, and restoration logs.", + "frequency": "yearly" } ] }, @@ -1046,7 +1120,8 @@ "id": "C1.1-policy", "type": "policy", "description": "Reference to the Data Classification Policy that outlines classification levels and handling requirements for confidential information.", - "policyId": "data_classification" + "policyId": "data_classification", + "frequency": "yearly" }, { "id": "C1.1-procedure", @@ -1057,7 +1132,8 @@ "id": "C1.1-evidence", "type": "evidence", "name": "Data Classification Records", - "description": "Data classification records, labeling practices, and access control lists for confidential information." + "description": "Data classification records, labeling practices, and access control lists for confidential information.", + "frequency": "yearly" } ] }, @@ -1072,7 +1148,8 @@ "id": "C1.2-policy", "type": "policy", "description": "Reference to the Data Classification Policy which includes controls for restricting access to confidential information.", - "policyId": "data_classification" + "policyId": "data_classification", + "frequency": "yearly" }, { "id": "C1.2-procedure", @@ -1083,7 +1160,8 @@ "id": "C1.2-evidence", "type": "evidence", "name": "Access Logs", - "description": "Access logs, periodic access reviews, and certification records." + "description": "Access logs, periodic access reviews, and certification records.", + "frequency": "quarterly" } ] }, @@ -1098,7 +1176,8 @@ "id": "C1.3-policy", "type": "policy", "description": "Reference to the Data Classification Policy which includes provisions for secure data disposal.", - "policyId": "data_classification" + "policyId": "data_classification", + "frequency": "yearly" }, { "id": "C1.3-procedure", @@ -1109,7 +1188,8 @@ "id": "C1.3-evidence", "type": "evidence", "name": "Disposal Records", - "description": "Disposal records, certificates of destruction, and audit logs." + "description": "Disposal records, certificates of destruction, and audit logs.", + "frequency": "yearly" } ] }, @@ -1124,7 +1204,8 @@ "id": "PI1.1-policy", "type": "policy", "description": "Reference to the Information Security Policy that addresses data accuracy and completeness.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "PI1.1-procedure", @@ -1135,7 +1216,8 @@ "id": "PI1.1-evidence", "type": "evidence", "name": "Data Validation Records", - "description": "Data validation reports, exception logs, and audit records." + "description": "Data validation reports, exception logs, and audit records.", + "frequency": "quarterly" } ] }, @@ -1150,7 +1232,8 @@ "id": "PI1.2-policy", "type": "policy", "description": "Reference to the Information Security Policy that outlines controls over data processing.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "PI1.2-procedure", @@ -1161,7 +1244,8 @@ "id": "PI1.2-evidence", "type": "evidence", "name": "Data Processing Logs", - "description": "Data processing logs, validation reports, and exception handling records." + "description": "Data processing logs, validation reports, and exception handling records.", + "frequency": "quarterly" } ] }, @@ -1176,7 +1260,8 @@ "id": "PI1.3-policy", "type": "policy", "description": "Reference to the Information Security Policy that outlines procedures for handling processing exceptions.", - "policyId": "information_security" + "policyId": "information_security", + "frequency": "yearly" }, { "id": "PI1.3-procedure", @@ -1187,7 +1272,8 @@ "id": "PI1.3-evidence", "type": "evidence", "name": "Exception Logs", - "description": "Exception logs, resolution documentation, and process improvement records." + "description": "Exception logs, resolution documentation, and process improvement records.", + "frequency": "quarterly" } ] }, @@ -1202,7 +1288,8 @@ "id": "P1.1-policy", "type": "policy", "description": "Reference to the Privacy Policy that informs individuals about personal data collection, usage, and disclosure practices.", - "policyId": "privacy" + "policyId": "privacy", + "frequency": "yearly" }, { "id": "P1.1-procedure", @@ -1213,7 +1300,8 @@ "id": "P1.1-evidence", "type": "evidence", "name": "Privacy Notice", - "description": "Copies of the privacy notice, version history, and distribution logs." + "description": "Copies of the privacy notice, version history, and distribution logs.", + "frequency": "yearly" } ] }, @@ -1228,7 +1316,8 @@ "id": "P1.2-policy", "type": "policy", "description": "Reference to the Privacy Policy that requires explicit consent for the collection and processing of personal data.", - "policyId": "privacy" + "policyId": "privacy", + "frequency": "yearly" }, { "id": "P1.2-procedure", @@ -1239,7 +1328,8 @@ "id": "P1.2-evidence", "type": "evidence", "name": "Consent Records", - "description": "Consent records, opt-in logs, and audit trails for consent management." + "description": "Consent records, opt-in logs, and audit trails for consent management.", + "frequency": "yearly" } ] }, @@ -1254,7 +1344,8 @@ "id": "P1.3-policy", "type": "policy", "description": "Reference to the Privacy Policy that defines retention periods and secure disposal methods for personal data.", - "policyId": "privacy" + "policyId": "privacy", + "frequency": "yearly" }, { "id": "P1.3-procedure", @@ -1265,7 +1356,8 @@ "id": "P1.3-evidence", "type": "evidence", "name": "Retention Schedules", - "description": "Retention schedules, disposal logs, and certificates of data destruction." + "description": "Retention schedules, disposal logs, and certificates of data destruction.", + "frequency": "yearly" } ] } diff --git a/packages/data/policies/access_control.json b/packages/data/policies/access_control.json index 776aa7d..139fd61 100644 --- a/packages/data/policies/access_control.json +++ b/packages/data/policies/access_control.json @@ -5,6 +5,7 @@ "slug": "access-control-policy", "name": "Access Control Policy", "description": "This policy defines the requirements for granting, monitoring, and revoking access to the organization’s information systems and data based on the principle of least privilege.", + "frequency": "yearly", "usedBy": { "soc2": ["CC6.1", "CC6.2", "CC6.3", "CC6.4", "CC6.5", "CC6.6"] } diff --git a/packages/data/policies/application_security.json b/packages/data/policies/application_security.json index 2a6ed09..448feeb 100644 --- a/packages/data/policies/application_security.json +++ b/packages/data/policies/application_security.json @@ -5,6 +5,7 @@ "slug": "application-security-policy", "name": "Application Security Policy", "description": "This policy outlines the security framework and requirements for applications, notably web applications, within the organization's production environment.", + "frequency": "yearly", "usedBy": { "soc2": ["CC7.1", "CC7.2", "CC7.4"] } diff --git a/packages/data/policies/availability.json b/packages/data/policies/availability.json index 97a0f1a..b3b54a6 100644 --- a/packages/data/policies/availability.json +++ b/packages/data/policies/availability.json @@ -5,6 +5,7 @@ "slug": "availability-policy", "name": "Availability Policy", "description": "This policy outlines the requirements for proper controls to protect the availability of the organization's information systems.", + "frequency": "yearly", "usedBy": { "soc2": ["CC9.1", "CC7.3", "CC7.5", "A1.1", "A1.2"] } diff --git a/packages/data/policies/business_continuity.json b/packages/data/policies/business_continuity.json index fa21451..45dda43 100644 --- a/packages/data/policies/business_continuity.json +++ b/packages/data/policies/business_continuity.json @@ -5,6 +5,7 @@ "slug": "business-continuity-dr-policy", "name": "Business Continuity & Disaster Recovery Policy", "description": "This policy outlines the strategies and procedures for ensuring the availability of critical systems and data during and after a disruptive event.", + "frequency": "yearly", "usedBy": { "soc2": ["CC7.3", "A1.3", "CC9.1", "CC9.9"] } diff --git a/packages/data/policies/change_management.json b/packages/data/policies/change_management.json index 4718ef0..f8fab5b 100644 --- a/packages/data/policies/change_management.json +++ b/packages/data/policies/change_management.json @@ -5,6 +5,7 @@ "slug": "change-management-policy", "name": "Change Management Policy", "description": "This policy defines the process for requesting, reviewing, approving, and documenting changes to the organization's information systems and infrastructure.", + "frequency": "yearly", "usedBy": { "soc2": ["CC3.4", "CC8.1", "CC6.7"] } diff --git a/packages/data/policies/classification.json b/packages/data/policies/classification.json index f3de13c..a85a96c 100644 --- a/packages/data/policies/classification.json +++ b/packages/data/policies/classification.json @@ -5,6 +5,7 @@ "slug": "data-classification-policy", "name": "Data Classification Policy", "description": "This policy outlines the requirements for data classification.", + "frequency": "yearly", "usedBy": { "soc2": ["CC6.1", "CC8.1", "CC6.6"] } diff --git a/packages/data/policies/code_of_conduct.json b/packages/data/policies/code_of_conduct.json index 22b6eac..b7a1031 100644 --- a/packages/data/policies/code_of_conduct.json +++ b/packages/data/policies/code_of_conduct.json @@ -5,6 +5,7 @@ "slug": "code-of-conduct", "name": "Code of Conduct Policy", "description": "This policy outlines the expected behavior from employees towards their colleagues, supervisors, and the organization as a whole.", + "frequency": "yearly", "usedBy": { "soc2": ["CC1.1", "CC6.1"] } diff --git a/packages/data/policies/confidentiality.json b/packages/data/policies/confidentiality.json index bc6b169..0eb842b 100644 --- a/packages/data/policies/confidentiality.json +++ b/packages/data/policies/confidentiality.json @@ -5,6 +5,7 @@ "slug": "confidentiality", "name": "Confidentiality Policy", "description": "This policy outlines the requirements for maintaining the confidentiality of sensitive and proprietary information within the organization.", + "frequency": "yearly", "usedBy": { "soc2": ["CC9.9", "CC6.1"] } diff --git a/packages/data/policies/corporate_governance.json b/packages/data/policies/corporate_governance.json index d9ec380..d204200 100644 --- a/packages/data/policies/corporate_governance.json +++ b/packages/data/policies/corporate_governance.json @@ -5,6 +5,7 @@ "slug": "corporate-governance-policy", "name": "Corporate Governance Policy", "description": "This policy defines the overall governance framework including board oversight, management responsibilities, and organizational structure to ensure effective oversight and accountability.", + "frequency": "yearly", "usedBy": { "soc2": ["CC1.1", "CC1.2", "CC1.5", "CC2.2", "CC2.3"] } diff --git a/packages/data/policies/cyber_risk.json b/packages/data/policies/cyber_risk.json index 72842d4..a67274f 100644 --- a/packages/data/policies/cyber_risk.json +++ b/packages/data/policies/cyber_risk.json @@ -5,6 +5,7 @@ "slug": "cyber-risk", "name": "Cyber Risk Assessment Policy", "description": "This policy outlines the requirements for conducting cyber risk assessments to identify, evaluate, and mitigate cybersecurity threats to the organization.", + "frequency": "yearly", "usedBy": { "soc2": ["CC1.1", "CC1.2", "CC1.3", "CC1.4", "CC1.5"] } diff --git a/packages/data/policies/data_center.json b/packages/data/policies/data_center.json index 568f705..e2d3784 100644 --- a/packages/data/policies/data_center.json +++ b/packages/data/policies/data_center.json @@ -5,6 +5,7 @@ "slug": "data-center", "name": "Data Center Policy", "description": "This policy outlines the requirements for the organization's data center facilities to ensure protection, availability, and reliability of critical systems and data.", + "frequency": "yearly", "usedBy": { "soc2": ["CC6.1", "CC6.2", "CC8.1", "CC7.1"] } diff --git a/packages/data/policies/data_classification.json b/packages/data/policies/data_classification.json index fa56251..70aa0f1 100644 --- a/packages/data/policies/data_classification.json +++ b/packages/data/policies/data_classification.json @@ -5,6 +5,7 @@ "slug": "data-classification-policy", "name": "Data Classification Policy", "description": "This policy establishes a framework for classifying data based on sensitivity and defines handling requirements for each classification level.", + "frequency": "yearly", "usedBy": { "soc2": ["C1.1", "C1.2", "C1.3"] } diff --git a/packages/data/policies/disaster_recovery.json b/packages/data/policies/disaster_recovery.json index 8ef6d48..2b96cd2 100644 --- a/packages/data/policies/disaster_recovery.json +++ b/packages/data/policies/disaster_recovery.json @@ -5,6 +5,7 @@ "slug": "disaster-recovery", "name": "Disaster Recovery Policy", "description": "This policy outlines the requirements for disaster recovery planning to ensure that critical business operations can be resumed in the event of a disruption.", + "frequency": "yearly", "usedBy": { "soc2": ["CC9.1", "CC8.1"] } diff --git a/packages/data/policies/human_resources.json b/packages/data/policies/human_resources.json index a3966bf..0feb351 100644 --- a/packages/data/policies/human_resources.json +++ b/packages/data/policies/human_resources.json @@ -5,6 +5,7 @@ "slug": "human-resources-policy", "name": "Human Resources Policy", "description": "This policy outlines the principles and practices for recruitment, employee management, performance evaluations, and the enforcement of internal control responsibilities.", + "frequency": "yearly", "usedBy": { "soc2": ["CC1.3", "CC1.4"] } diff --git a/packages/data/policies/incident_response.json b/packages/data/policies/incident_response.json index a6b88d8..f384e2e 100644 --- a/packages/data/policies/incident_response.json +++ b/packages/data/policies/incident_response.json @@ -5,6 +5,7 @@ "slug": "incident-response-policy", "name": "Incident Response Policy", "description": "This policy establishes the framework and procedures for detecting, responding to, and recovering from security incidents.", + "frequency": "yearly", "usedBy": { "soc2": ["CC7.2", "CC7.4", "CC7.5"] } diff --git a/packages/data/policies/information_security.json b/packages/data/policies/information_security.json index 07a5b96..60421a0 100644 --- a/packages/data/policies/information_security.json +++ b/packages/data/policies/information_security.json @@ -5,6 +5,7 @@ "slug": "information-security-policy", "name": "Information Security Policy", "description": "This policy establishes the framework for protecting the organization's information assets by defining security objectives, roles, responsibilities, and controls.", + "frequency": "yearly", "usedBy": { "soc2": ["CC2.1", "PI1.1", "PI1.2", "PI1.3", "CC5.2"] } diff --git a/packages/data/policies/password_policy.json b/packages/data/policies/password_policy.json index b21b3cf..e541449 100644 --- a/packages/data/policies/password_policy.json +++ b/packages/data/policies/password_policy.json @@ -4,6 +4,7 @@ "slug": "password-policy", "name": "Password Policy", "description": "This policy outlines the requirements for passwords used by employees.", + "frequency": "yearly", "usedBy": { "soc2": ["CC1.1", "CC1.2", "CC1.3"] } diff --git a/packages/data/policies/privacy.json b/packages/data/policies/privacy.json index 38190f4..032c13f 100644 --- a/packages/data/policies/privacy.json +++ b/packages/data/policies/privacy.json @@ -5,6 +5,7 @@ "slug": "privacy-policy", "name": "Privacy Policy", "description": "This policy describes how the organization collects, uses, discloses, and protects personal information in compliance with applicable privacy regulations.", + "frequency": "yearly", "usedBy": { "soc2": ["P1.1", "P1.2", "P1.3"] } diff --git a/packages/data/policies/risk_assessment.json b/packages/data/policies/risk_assessment.json index 3c7208b..3d34b31 100644 --- a/packages/data/policies/risk_assessment.json +++ b/packages/data/policies/risk_assessment.json @@ -5,6 +5,7 @@ "slug": "risk-assessment", "name": "Risk Assessment Policy", "description": "This policy outlines the requirements for conducting risk assessments to identify, evaluate, and mitigate risks associated with the organization's information systems, operations, and assets.", + "frequency": "yearly", "usedBy": { "soc2": ["CC3.2", "CC3.4", "CC8.1"] } diff --git a/packages/data/policies/risk_management.json b/packages/data/policies/risk_management.json index 2bf7257..60c62f9 100644 --- a/packages/data/policies/risk_management.json +++ b/packages/data/policies/risk_management.json @@ -5,6 +5,7 @@ "slug": "risk-management-policy", "name": "Risk Management Policy", "description": "This policy defines the process for identifying, assessing, and mitigating risks to the organization’s objectives and information assets.", + "frequency": "yearly", "usedBy": { "soc2": ["CC3.1", "CC3.2", "CC3.3", "CC4.2"] } diff --git a/packages/data/policies/software_development.json b/packages/data/policies/software_development.json index a8f4b91..4797940 100644 --- a/packages/data/policies/software_development.json +++ b/packages/data/policies/software_development.json @@ -5,6 +5,7 @@ "slug": "software-development", "name": "Software Development Lifecycle Policy", "description": "This policy outlines the requirements for the software development lifecycle to ensure secure, reliable, and high-quality software development practices.", + "frequency": "yearly", "usedBy": { "soc2": ["CC6.2", "CC7.1", "CC7.2", "CC8.1"] } diff --git a/packages/data/policies/system_change.json b/packages/data/policies/system_change.json index 8427464..de9dbf4 100644 --- a/packages/data/policies/system_change.json +++ b/packages/data/policies/system_change.json @@ -5,6 +5,7 @@ "slug": "system-change-policy", "name": "System Change Policy", "description": "This policy outlines the requirements for system changes.", + "frequency": "yearly", "usedBy": { "soc2": ["CC3.4", "CC6.8", "CC7.1", "A1.1"] } diff --git a/packages/data/policies/thirdparty.json b/packages/data/policies/thirdparty.json index 2092162..9a25801 100644 --- a/packages/data/policies/thirdparty.json +++ b/packages/data/policies/thirdparty.json @@ -5,6 +5,7 @@ "slug": "thirdparty", "name": "Third-Party Management Policy", "description": "This policy defines the rules for relationships with the organization’s Information Technology (IT) third-parties and partners.", + "frequency": "yearly", "usedBy": { "soc2": ["CC2.3", "CC7.3", "CC8.1"] } diff --git a/packages/data/policies/vendor_risk_management.json b/packages/data/policies/vendor_risk_management.json index bc9b1e4..48d7ba7 100644 --- a/packages/data/policies/vendor_risk_management.json +++ b/packages/data/policies/vendor_risk_management.json @@ -5,6 +5,7 @@ "slug": "vendor-risk-management-policy", "name": "Vendor Risk Management Policy", "description": "This policy outlines the criteria and procedures for evaluating, selecting, and monitoring third-party vendors to manage risks associated with external service providers.", + "frequency": "yearly", "usedBy": { "soc2": ["CC9.2"] } diff --git a/packages/data/policies/workstation.json b/packages/data/policies/workstation.json index 3133394..858d7d2 100644 --- a/packages/data/policies/workstation.json +++ b/packages/data/policies/workstation.json @@ -5,6 +5,7 @@ "slug": "workstation", "name": "Workstation Policy", "description": "This policy outlines the requirements for workstations to ensure secure, reliable, and high-quality software development practices.", + "frequency": "yearly", "usedBy": { "soc2": ["CC6.2", "CC6.7", "CC7.2"] } diff --git a/packages/db/prisma/migrations/20250224173509_add_frequency_to_policy_and_evidence/migration.sql b/packages/db/prisma/migrations/20250224173509_add_frequency_to_policy_and_evidence/migration.sql new file mode 100644 index 0000000..5a7c2ba --- /dev/null +++ b/packages/db/prisma/migrations/20250224173509_add_frequency_to_policy_and_evidence/migration.sql @@ -0,0 +1,10 @@ +-- CreateEnum +CREATE TYPE "Frequency" AS ENUM ('monthly', 'quarterly', 'yearly'); + +-- AlterTable +ALTER TABLE "OrganizationEvidence" ADD COLUMN "frequency" "Frequency", +ADD COLUMN "lastPublishedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "OrganizationPolicy" ADD COLUMN "frequency" "Frequency", +ADD COLUMN "lastPublishedAt" TIMESTAMP(3); diff --git a/packages/db/prisma/migrations/20250224174548_add_frequency_to_evidence/migration.sql b/packages/db/prisma/migrations/20250224174548_add_frequency_to_evidence/migration.sql new file mode 100644 index 0000000..4a66d8f --- /dev/null +++ b/packages/db/prisma/migrations/20250224174548_add_frequency_to_evidence/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Evidence" ADD COLUMN "frequency" "Frequency"; diff --git a/packages/db/prisma/migrations/20250224180117_add_frequency_to_policy/migration.sql b/packages/db/prisma/migrations/20250224180117_add_frequency_to_policy/migration.sql new file mode 100644 index 0000000..a081b6a --- /dev/null +++ b/packages/db/prisma/migrations/20250224180117_add_frequency_to_policy/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Policy" ADD COLUMN "frequency" "Frequency"; diff --git a/packages/db/prisma/migrations/20250224183431_add_frequency_to_control_requirement/migration.sql b/packages/db/prisma/migrations/20250224183431_add_frequency_to_control_requirement/migration.sql new file mode 100644 index 0000000..32a286c --- /dev/null +++ b/packages/db/prisma/migrations/20250224183431_add_frequency_to_control_requirement/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "ControlRequirement" ADD COLUMN "frequency" "Frequency"; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index c969010..7b5d1b5 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -38,6 +38,12 @@ enum RequirementType { training } +enum Frequency { + monthly + quarterly + yearly +} + model Account { id String @id @default(cuid()) userId String @@ -847,15 +853,16 @@ model Employee { // --- Policy Templates and Files --- model Policy { - id String @id @default(cuid()) - slug String @unique + id String @id @default(cuid()) + slug String @unique name String description String? content Json[] version String? usedBy Json - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + frequency Frequency? // Relations policyFrameworks PolicyFramework[] @@ -927,6 +934,8 @@ model ControlRequirement { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + frequency Frequency? + controlId String control Control @relation(fields: [controlId], references: [id], onDelete: Cascade) @@ -976,9 +985,12 @@ model OrganizationPolicy { organizationId String organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) - status PolicyStatus @default(draft) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + status PolicyStatus @default(draft) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + lastPublishedAt DateTime? + + frequency Frequency? policyId String policy Policy @relation(fields: [policyId], references: [id], onDelete: Cascade) @@ -1012,9 +1024,10 @@ model OrganizationCategory { // Evidence metadata model Evidence { - id String @id @default(cuid()) + id String @id @default(cuid()) name String description String? + frequency Frequency? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -1032,7 +1045,11 @@ model OrganizationEvidence { description String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - published Boolean @default(false) + + published Boolean @default(false) + lastPublishedAt DateTime? + + frequency Frequency? fileUrls String[] additionalUrls String[] diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts index 23637dd..13bd1ea 100644 --- a/packages/db/prisma/seed.ts +++ b/packages/db/prisma/seed.ts @@ -1,5 +1,5 @@ import { PrismaClient } from "@prisma/client"; -import type { Prisma } from "@prisma/client"; +import type { Frequency, Prisma } from "@prisma/client"; import { RequirementType } from "@prisma/client"; import { readFileSync, readdirSync } from "node:fs"; import { join } from "node:path"; @@ -21,6 +21,7 @@ async function main() { await prisma.organizationControl.deleteMany(); await prisma.organizationPolicy.deleteMany(); await prisma.organizationControlRequirement.deleteMany(); + await prisma.organizationEvidence.deleteMany(); await prisma.policy.deleteMany(); await prisma.policyControl.deleteMany(); @@ -32,6 +33,8 @@ async function main() { await prisma.framework.deleteMany(); await prisma.frameworkCategory.deleteMany(); + await prisma.evidence.deleteMany(); + console.log("✅ Database cleaned"); } @@ -107,6 +110,7 @@ async function seedPolicies() { description: policyData.metadata.description, content: policyData.content as Prisma.InputJsonValue[], usedBy: policyData.metadata.usedBy as Prisma.InputJsonValue, + frequency: policyData.metadata?.frequency ?? null, }, create: { id: policyData.metadata.id, @@ -115,6 +119,7 @@ async function seedPolicies() { description: policyData.metadata.description, content: policyData.content as Prisma.InputJsonValue[], usedBy: policyData.metadata.usedBy as Prisma.InputJsonValue, + frequency: policyData.metadata?.frequency ?? null, }, }); console.log(` ✅ ${file} processed`); @@ -289,6 +294,7 @@ async function seedFrameworkCategoryControls( (requirement.type as RequirementType) === "policy" ? requirement.policyId : null, + frequency: requirement?.frequency ?? null, }, update: { name: requirement.name, @@ -297,6 +303,7 @@ async function seedFrameworkCategoryControls( (requirement.type as RequirementType) === "policy" ? requirement.policyId : null, + frequency: requirement?.frequency ?? null, }, }); } @@ -378,21 +385,23 @@ async function seedEvidence() { console.log(`🔄 Processing ${evidenceRequirements.length} evidences`); - for (const evidence of evidenceRequirements) { - console.log(` ⏳ Processing evidence: ${evidence.name}...`); + for (const evidenceReq of evidenceRequirements) { + console.log(` ⏳ Processing evidence: ${evidenceReq.name}...`); await prisma.evidence.upsert({ where: { - id: evidence.id, + id: evidenceReq.id, }, update: { - name: evidence.name, - description: evidence.description, + name: evidenceReq.name, + description: evidenceReq.description, + frequency: evidenceReq.frequency ?? null, }, create: { - id: evidence.id, - name: evidence.name, - description: evidence.description, + id: evidenceReq.id, + name: evidenceReq.name, + description: evidenceReq.description, + frequency: evidenceReq.frequency ?? null, }, }); } diff --git a/packages/db/prisma/seedTypes.ts b/packages/db/prisma/seedTypes.ts index 557a942..e272738 100644 --- a/packages/db/prisma/seedTypes.ts +++ b/packages/db/prisma/seedTypes.ts @@ -1,4 +1,4 @@ -import type { JsonArray } from "@prisma/client/runtime/library"; +import type { Frequency } from "@prisma/client"; export interface Framework { name: string; @@ -18,6 +18,7 @@ export interface Requirement { description: string; policyId?: string; name?: string; + frequency?: Frequency; } export interface Control { @@ -46,6 +47,7 @@ export interface Policy { usedBy: { [key: string]: string[]; }; + frequency?: Frequency; }; content: Array<{ type: string;