Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/app/src/actions/framework/select-frameworks-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { db, type Policy, type User } from "@bubba/db";
import { authActionClient } from "../safe-action";
import { z } from "zod";
import type { ActionData } from "../types";
import type { InputJsonValue } from "@prisma/client/runtime/library";

const selectFrameworksSchema = z.object({
frameworkIds: z.array(z.string()),
Expand Down Expand Up @@ -136,6 +137,7 @@ const createOrganizationPolicy = async (user: User, frameworkIds: string[]) => {
organizationId: user.organizationId!,
policyId: policy.id,
status: "draft",
content: policy.content as InputJsonValue[],
})),
});

Expand Down
12 changes: 6 additions & 6 deletions apps/app/src/actions/policies/update-policy-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface ContentNode {

// Simplified content processor that creates a new plain object
function processContent(
content: ContentNode | ContentNode[],
content: ContentNode | ContentNode[]
): ContentNode | ContentNode[] {
if (!content) return content;

Expand Down Expand Up @@ -74,8 +74,8 @@ export const updatePolicyAction = authActionClient
}

try {
const policy = await db.artifact.findUnique({
where: { id, type: "policy", organizationId: user.organizationId },
const policy = await db.organizationPolicy.findUnique({
where: { id, organizationId: user.organizationId },
});

if (!policy) {
Expand All @@ -87,12 +87,12 @@ export const updatePolicyAction = authActionClient

// Create a new plain object from the content
const processedContent = JSON.parse(
JSON.stringify(processContent(content as ContentNode)),
JSON.stringify(processContent(content as ContentNode))
);

await db.artifact.update({
await db.organizationPolicy.update({
where: { id },
data: { content: processedContent },
data: { content: processedContent.content },
});

revalidatePath(`/policies/${id}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use client";

import { PoliciesByAssignee } from "@/components/policies/charts/policies-by-assignee";
import { PoliciesByFramework } from "@/components/policies/charts/policies-by-framework";
import { usePolicies } from "../hooks/usePolicies";
import { Skeleton } from "@bubba/ui/skeleton";
import { usePolicies } from "../../hooks/usePolicies";

export function PoliciesOverview() {
const { data, isLoading, error } = usePolicies();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use server";

import { authActionClient } from "@/actions/safe-action";
import { db, type OrganizationPolicy } from "@bubba/db";
import { z } from "zod";

const schema = z.object({
policyId: z.string(),
});

export type PolicyStatsResponse = {
success: boolean;
data?: OrganizationPolicy[];
error?: string;
};
Comment on lines +11 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the response type definition.

The PolicyStatsResponse type's data property should be a single OrganizationPolicy or undefined, not an array, as the findFirst query returns a single record.

 export type PolicyStatsResponse = {
   success: boolean;
-  data?: OrganizationPolicy[];
+  data?: OrganizationPolicy;
   error?: string;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type PolicyStatsResponse = {
success: boolean;
data?: OrganizationPolicy[];
error?: string;
};
export type PolicyStatsResponse = {
success: boolean;
data?: OrganizationPolicy;
error?: string;
};


export const getPolicy = authActionClient
.schema(schema)
.metadata({
name: "get-policy",
track: {
event: "get-policy",
channel: "server",
},
})
.action(async ({ ctx, parsedInput }) => {
const { user } = ctx;
const { policyId } = parsedInput;

if (!user.organizationId) {
return {
success: false,
error: "Not authorized - no organization found",
};
}

try {
const policy = await db.organizationPolicy.findFirst({
where: {
organizationId: user.organizationId!,
id: policyId,
},
include: {
policy: true,
},
});
Comment on lines +37 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add null check and improve error handling.

The database query could be improved with better error handling and null checks.

 try {
   const policy = await db.organizationPolicy.findFirst({
     where: {
-      organizationId: user.organizationId!,
+      organizationId: user.organizationId,
       id: policyId,
     },
     include: {
       policy: true,
     },
   });

+  if (!policy) {
+    return {
+      success: false,
+      error: "Policy not found",
+    };
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const policy = await db.organizationPolicy.findFirst({
where: {
organizationId: user.organizationId!,
id: policyId,
},
include: {
policy: true,
},
});
try {
const policy = await db.organizationPolicy.findFirst({
where: {
organizationId: user.organizationId,
id: policyId,
},
include: {
policy: true,
},
});
if (!policy) {
return {
success: false,
error: "Policy not found",
};
}


return {
success: true,
data: policy,
};
} catch (error) {
return {
success: false,
error: "Failed to fetch policy statistics",
};
}
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { auth } from "@/auth";
import { Title } from "@/components/title";
import { getI18n } from "@/locales/server";
import { db } from "@bubba/db";
import { unstable_cache } from "next/cache";
import { redirect } from "next/navigation";

export default async function Layout({
Expand All @@ -12,7 +8,6 @@ export default async function Layout({
children: React.ReactNode;
params: Promise<{ id: string }>;
}) {
const t = await getI18n();
const { id } = await params;
const session = await auth();

Expand All @@ -24,40 +19,9 @@ export default async function Layout({
redirect("/policies");
}

const policy = await getPolicy(id, session.user.organizationId);

if (!policy) {
redirect("/policies");
}

return (
<div className="max-w-[1200px] space-y-4">
<Title title={policy.name} href="/policies/all" />

<main className="h-[calc(100vh-4rem-4rem)]">{children}</main>
</div>
);

/* return (
<div className="h-[calc(100vh-4rem)] bg-background max-w-[1200px]">
<div className="h-16 px-4 border-b flex items-center">
<Title title={policy.name} href="/policies/all" />
</div>
<main className="h-[calc(100vh-4rem-4rem)]">{children}</main>
</div>
); */
}

const getPolicy = unstable_cache(
async (policyId: string, organizationId: string) => {
const policy = await db.artifact.findUnique({
where: {
id: policyId,
organizationId: organizationId,
},
});

return policy;
},
["policy-cache"],
);
Original file line number Diff line number Diff line change
@@ -1,58 +1,13 @@
import { auth } from "@/auth";
import { PolicyOverview } from "@/components/policies/policy-overview";
import { db } from "@bubba/db";
import { unstable_cache } from "next/cache";
import { redirect } from "next/navigation";

interface PageProps {
params: Promise<{ id: string }>;
}

export default async function PolicyPage({ params }: PageProps) {
const session = await auth();
const { id } = await params;

if (!session) {
redirect("/login");
}

if (!session.user.organizationId || !id) {
redirect("/");
}

const policy = await getPolicy(id, session.user.organizationId);

if (!policy) {
redirect("/policies");
}

const users = await getUsers(session.user.organizationId);

return <PolicyOverview policy={policy} />;
return <PolicyOverview policyId={id} />;
}

const getPolicy = unstable_cache(
async (id: string, organizationId: string) => {
const policy = await db.artifact.findUnique({
where: {
id,
type: "policy",
organizationId: organizationId,
},
});

return policy;
},
["policy-cache"],
);

const getUsers = unstable_cache(
async (organizationId: string) => {
const users = await db.user.findMany({
where: { organizationId: organizationId },
});

return users;
},
["users-cache"],
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { getPolicy } from "../Actions/get-policy";
import useSWR from "swr";
import type { OrganizationPolicy, Policy } from "@bubba/db";

const POLICY_KEY = "policy";

type OrganizationPolicyWithPolicy = OrganizationPolicy & {
policy: Policy;
};

async function fetchPolicy(
policyId: string
): Promise<OrganizationPolicyWithPolicy> {
const response = await getPolicy({ policyId });

if (!response?.data?.success || !response.data.data) {
throw new Error(response?.data?.error || "Failed to fetch policy data");
}

return response.data.data;
}

export function usePolicy({ policyId }: { policyId: string }) {
const { data, error, isLoading, mutate } =
useSWR<OrganizationPolicyWithPolicy>(
[POLICY_KEY, policyId],
() => fetchPolicy(policyId),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);

return {
data,
isLoading,
error,
mutate,
};
}
12 changes: 9 additions & 3 deletions apps/app/src/components/editor/advanced-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ const extensions = [...defaultExtensions, slashCommand];
const PolicyEditor = ({
policyId,
content,
}: { policyId: string; content: JSONContent }) => {
const [initialContent, setInitialContent] = useState<JSONContent>(content);
}: {
policyId: string;
content: JSONContent;
}) => {
const [initialContent, setInitialContent] = useState<JSONContent>({
type: "doc",
content: content as JSONContent[],
});
const [saveStatus, setSaveStatus] = useState("Saved");
const [charsCount, setCharsCount] = useState();

Expand Down Expand Up @@ -74,7 +80,7 @@ const PolicyEditor = ({
content: json,
});
},
1000,
1000
);

if (!initialContent) return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { usePolicies } from "@/app/[locale]/(app)/(dashboard)/policies/(overview)/hooks/usePolicies";
import { usePolicies } from "@/app/[locale]/(app)/(dashboard)/policies/hooks/usePolicies";
import { useI18n } from "@/locales/client";
import { Card, CardContent, CardHeader, CardTitle } from "@bubba/ui/card";
import {
Expand Down
10 changes: 7 additions & 3 deletions apps/app/src/components/policies/policy-overview.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
"use client";

import type { Artifact } from "@bubba/db";
import type { JSONContent } from "@tiptap/react";
import PolicyEditor from "../editor/advanced-editor";
import { usePolicy } from "@/app/[locale]/(app)/(dashboard)/policies/hooks/usePolicy";

export function PolicyOverview({ policyId }: { policyId: string }) {
const { data: policy } = usePolicy({ policyId });

if (!policy) return null;

export function PolicyOverview({ policy }: { policy: Artifact }) {
const content = policy.content as JSONContent;

if (!content) return null;

return (
<div className="h-[calc(100vh-8rem)] flex flex-col">
<PolicyEditor policyId={policy.id} content={content} />
<PolicyEditor policyId={policyId} content={content} />
</div>
);
}
10 changes: 9 additions & 1 deletion apps/app/src/jobs/seed/policies/access.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
{
"type": "doc",
"metadata": {
"controls": ["CC6.1", "CC6.2", "CC6.3"]
"id": "access-onboarding-and-termination-policy",
"slug": "access-onboarding-and-termination-policy",
"name": "Access Onboarding and Termination Policy",
"description": "This policy outlines the procedures for onboarding and offboarding users to technical infrastructure.",
"usedBy": {
"soc2": {
"CC6": ["CC6.1", "CC6.2", "CC6.3"]
}
}
Comment on lines +8 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

⚠️ Potential issue

🧩 Analysis chain

Resolve inconsistencies in SOC2 control mappings.

The SOC2 control mappings in this file differ from its duplicate in packages/data/policies/access.json:

  • This file: CC6.1, CC6.2, CC6.3 under CC6
  • Duplicate: CC6.1, CC6.2, CC6.4, CC6.8

Please clarify which file is the source of truth and ensure consistent control mappings across both files.

Run the following script to identify all policy files and their SOC2 control mappings:


🏁 Script executed:

#!/bin/bash
# Description: Find all policy JSON files and extract their SOC2 control mappings.

# Find all policy JSON files
fd -e json -t f . apps/app/src/jobs/seed/policies packages/data/policies --exec sh -c '
  echo "=== $1 ==="
  jq -r ".metadata.usedBy.soc2 | if type == \"object\" then to_entries[] | \"\(.key): \(.value | join(\", \"))\" else join(\", \") end" "$1"
  echo
' _

Length of output: 1410


SOC2 Control Mapping Mismatch – Align Seed Policy with Authoritative Mapping

The seed policy in apps/app/src/jobs/seed/policies/access.json shows the SOC2 mapping for CC6 as “CC6.1, CC6.2, CC6.3”, while the duplicate in packages/data/policies/access.json maps CC6 as “CC6.1, CC6.2, CC6.4, CC6.8”. This discrepancy confirms the review comment’s concern. Given that the mapping in packages/data/policies/access.json appears in the central data package and is used by other policy files (e.g., see consistent detailed control sets in other packages/data files), it should be treated as the source of truth. Accordingly, update the seed file to match the mappings in packages/data/policies/access.json for consistency.

  • File Differences:

    • Seed File (apps/app/src/jobs/seed/policies/access.json): CC6 → CC6.1, CC6.2, CC6.3
    • Data Package (packages/data/policies/access.json): CC6 → CC6.1, CC6.2, CC6.4, CC6.8
  • Action:

    • Confirm that packages/data/policies/access.json is indeed the authoritative source.
    • Update the SOC2 control mappings in apps/app/src/jobs/seed/policies/access.json to align with the authoritative mappings.

},
"content": [
{
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/jobs/seed/policies/application.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"type": "doc",
"metadata": {
"controls": ["CC6.2"]
"controls": ["CC7.1", "CC7.2", "CC7.4"]
},
"content": [
{
Expand Down
20 changes: 20 additions & 0 deletions packages/data/categories/soc2.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,25 @@
"name": "CC9: Risk Mitigation",
"code": "CC9",
"description": "This criterion ensures that the organization has a process for mitigating risks to its security posture."
},
"A1": {
"name": "A1: Availability",
"code": "A1",
"description": "This criterion ensures that systems are available for operation and use as committed or agreed."
},
"C1": {
"name": "C1: Data Integrity",
"code": "C1",
"description": "This criterion ensures that data is accurate, complete, and consistent."
},
"PI1": {
"name": "PI1: Processing Integrity",
"code": "PI1",
"description": "This criterion ensures that data is accurate, complete, and consistent."
},
"P1": {
"name": "P1: Privacy",
"code": "P1",
"description": "This criterion ensures that data is accurate, complete, and consistent."
}
Comment on lines +47 to 66
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update descriptions for C1, PI1, and P1 categories.

The descriptions for C1 (Data Integrity), PI1 (Processing Integrity), and P1 (Privacy) are identical. Each category should have a unique description that reflects its specific focus:

  • C1 should focus on data integrity controls
  • PI1 should emphasize system processing accuracy
  • P1 should detail privacy protection measures

Suggested updates:

  "C1": {
    "name": "C1: Data Integrity",
    "code": "C1",
-   "description": "This criterion ensures that data is accurate, complete, and consistent."
+   "description": "This criterion ensures that stored data maintains its integrity through controls preventing unauthorized modification or corruption."
  },
  "PI1": {
    "name": "PI1: Processing Integrity",
    "code": "PI1",
-   "description": "This criterion ensures that data is accurate, complete, and consistent."
+   "description": "This criterion ensures that system processing is complete, valid, accurate, timely, and authorized."
  },
  "P1": {
    "name": "P1: Privacy",
    "code": "P1",
-   "description": "This criterion ensures that data is accurate, complete, and consistent."
+   "description": "This criterion ensures that personal information is collected, used, retained, disclosed, and disposed of in accordance with privacy commitments and requirements."
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"A1": {
"name": "A1: Availability",
"code": "A1",
"description": "This criterion ensures that systems are available for operation and use as committed or agreed."
},
"C1": {
"name": "C1: Data Integrity",
"code": "C1",
"description": "This criterion ensures that data is accurate, complete, and consistent."
},
"PI1": {
"name": "PI1: Processing Integrity",
"code": "PI1",
"description": "This criterion ensures that data is accurate, complete, and consistent."
},
"P1": {
"name": "P1: Privacy",
"code": "P1",
"description": "This criterion ensures that data is accurate, complete, and consistent."
}
"A1": {
"name": "A1: Availability",
"code": "A1",
"description": "This criterion ensures that systems are available for operation and use as committed or agreed."
},
"C1": {
"name": "C1: Data Integrity",
"code": "C1",
"description": "This criterion ensures that stored data maintains its integrity through controls preventing unauthorized modification or corruption."
},
"PI1": {
"name": "PI1: Processing Integrity",
"code": "PI1",
"description": "This criterion ensures that system processing is complete, valid, accurate, timely, and authorized."
},
"P1": {
"name": "P1: Privacy",
"code": "P1",
"description": "This criterion ensures that personal information is collected, used, retained, disclosed, and disposed of in accordance with privacy commitments and requirements."
}

}
Loading