Skip to content
Closed
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: 1 addition & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"use-long-press": "^3.3.0",
"xml2js": "^0.6.2",
"zaraz-ts": "^1.2.0",
"zod": "^4.0.17",
"zod": "^3.25.76",
"zustand": "^5.0.3"
},
"devDependencies": {
Expand Down
24 changes: 13 additions & 11 deletions apps/app/src/actions/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const organizationWebsiteSchema = z.object({
export const createRiskSchema = z.object({
title: z
.string({
error: 'Risk name is required',
required_error: 'Risk name is required',
})
.min(1, {
message: 'Risk name should be at least 1 character',
Expand All @@ -75,7 +75,7 @@ export const createRiskSchema = z.object({
}),
description: z
.string({
error: 'Risk description is required',
required_error: 'Risk description is required',
})
.min(1, {
message: 'Risk description should be at least 1 character',
Expand All @@ -84,10 +84,10 @@ export const createRiskSchema = z.object({
message: 'Risk description should be at most 255 characters',
}),
category: z.nativeEnum(RiskCategory, {
error: 'Risk category is required',
required_error: 'Risk category is required',
}),
department: z.nativeEnum(Departments, {
error: 'Risk department is required',
required_error: 'Risk department is required',
}),
assigneeId: z.string().optional().nullable(),
});
Expand All @@ -103,14 +103,14 @@ export const updateRiskSchema = z.object({
message: 'Risk description is required',
}),
category: z.nativeEnum(RiskCategory, {
error: 'Risk category is required',
required_error: 'Risk category is required',
}),
department: z.nativeEnum(Departments, {
error: 'Risk department is required',
required_error: 'Risk department is required',
}),
assigneeId: z.string().optional().nullable(),
status: z.nativeEnum(RiskStatus, {
error: 'Risk status is required',
required_error: 'Risk status is required',
}),
});

Expand Down Expand Up @@ -162,7 +162,7 @@ export const updateTaskSchema = z.object({
description: z.string().optional(),
dueDate: z.date().optional(),
status: z.nativeEnum(TaskStatus, {
error: 'Task status is required',
required_error: 'Task status is required',
}),
assigneeId: z.string().optional().nullable(),
});
Expand Down Expand Up @@ -251,8 +251,10 @@ export const updateResidualRiskEnumSchema = z.object({

// Policies
export const createPolicySchema = z.object({
title: z.string({ error: 'Title is required' }).min(1, 'Title is required'),
description: z.string({ error: 'Description is required' }).min(1, 'Description is required'),
title: z.string({ required_error: 'Title is required' }).min(1, 'Title is required'),
description: z
.string({ required_error: 'Description is required' })
.min(1, 'Description is required'),
frameworkIds: z.array(z.string()).optional(),
controlIds: z.array(z.string()).optional(),
entityId: z.string().optional(),
Expand All @@ -279,7 +281,7 @@ export const createEmployeeSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email address'),
department: z.nativeEnum(Departments, {
error: 'Department is required',
required_error: 'Department is required',
}),
externalEmployeeId: z.string().optional(),
isActive: z.boolean().default(true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const createVendorTaskSchema = z.object({
message: 'Description is required',
}),
dueDate: z.date({
error: 'Due date is required',
required_error: 'Due date is required',
}),
assigneeId: z.string().nullable(),
});
Expand Down Expand Up @@ -79,7 +79,7 @@ export const updateVendorTaskSchema = z.object({
}),
dueDate: z.date().optional(),
status: z.nativeEnum(TaskStatus, {
error: 'Task status is required',
required_error: 'Task status is required',
}),
assigneeId: z.string().nullable(),
});
161 changes: 126 additions & 35 deletions apps/app/src/jobs/tasks/onboarding/onboard-organization-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import {
Likelihood,
Risk,
RiskCategory,
RiskStatus,
RiskTreatmentType,
VendorCategory,
} from '@db';
import { logger, tasks } from '@trigger.dev/sdk';
import { generateObject, generateText } from 'ai';
import { generateObject, generateText, jsonSchema } from 'ai';
import axios from 'axios';
import z from 'zod';
import type { researchVendor } from '../scrape/research';
import { RISK_MITIGATION_PROMPT } from './prompts/risk-mitigation';
import { VENDOR_RISK_ASSESSMENT_PROMPT } from './prompts/vendor-risk-assessment';
Expand Down Expand Up @@ -53,6 +53,58 @@ export type RiskData = {
department: Departments;
};

// Baseline risks that must always exist for every organization regardless of frameworks
const BASELINE_RISKS: Array<{
title: string;
description: string;
category: RiskCategory;
department: Departments;
status: RiskStatus;
}> = [
{
title: 'Intentional Fraud and Misuse',
description:
'Intentional misrepresentation or deception by an internal actor (employee, contractor) or by the organization as a whole, for the purpose of achieving an unauthorized or improper gain.',
category: RiskCategory.governance,
department: Departments.gov,
status: RiskStatus.closed,
},
];

/**
* Ensures baseline risks are present for the organization.
* Creates them if missing. Returns the list of risks that were created.
*/
export async function ensureBaselineRisks(organizationId: string): Promise<Risk[]> {
const created: Risk[] = [];

for (const base of BASELINE_RISKS) {
const existing = await db.risk.findFirst({
where: {
organizationId,
title: base.title,
},
});

if (!existing) {
const risk = await db.risk.create({
data: {
title: base.title,
description: base.description,
category: base.category,
department: base.department,
status: base.status,
organizationId,
},
});
created.push(risk);
logger.info(`Created baseline risk: ${risk.id} (${risk.title})`);
}
}

return created;
}

/**
* Revalidates the organization path for cache busting
*/
Expand Down Expand Up @@ -114,28 +166,47 @@ export async function getOrganizationContext(organizationId: string) {
export async function extractVendorsFromContext(
questionsAndAnswers: ContextItem[],
): Promise<VendorData[]> {
const result = await generateObject({
const { object } = await generateObject({
model: openai('gpt-4.1-mini'),
schema: z.object({
vendors: z.array(
z.object({
vendor_name: z.string(),
vendor_website: z.string(),
vendor_description: z.string(),
category: z.enum(Object.values(VendorCategory) as [string, ...string[]]),
inherent_probability: z.enum(Object.values(Likelihood) as [string, ...string[]]),
inherent_impact: z.enum(Object.values(Impact) as [string, ...string[]]),
residual_probability: z.enum(Object.values(Likelihood) as [string, ...string[]]),
residual_impact: z.enum(Object.values(Impact) as [string, ...string[]]),
}),
),
mode: 'json',
schema: jsonSchema({
type: 'object',
properties: {
vendors: {
type: 'array',
items: {
type: 'object',
properties: {
vendor_name: { type: 'string' },
vendor_website: { type: 'string' },
vendor_description: { type: 'string' },
category: { type: 'string', enum: Object.values(VendorCategory) },
inherent_probability: { type: 'string', enum: Object.values(Likelihood) },
inherent_impact: { type: 'string', enum: Object.values(Impact) },
residual_probability: { type: 'string', enum: Object.values(Likelihood) },
residual_impact: { type: 'string', enum: Object.values(Impact) },
},
required: [
'vendor_name',
'vendor_website',
'vendor_description',
'category',
'inherent_probability',
'inherent_impact',
'residual_probability',
'residual_impact',
],
},
},
},
required: ['vendors'],
}),
system:
'Extract vendor names from the following questions and answers. Return their name (grammar-correct), website, description, category, inherent probability, inherent impact, residual probability, and residual impact.',
prompt: questionsAndAnswers.map((q) => `${q.question}\n${q.answer}`).join('\n'),
});

return result.object.vendors as VendorData[];
return (object as { vendors: VendorData[] }).vendors;
}

/**
Expand Down Expand Up @@ -335,23 +406,40 @@ export async function extractRisksFromContext(
organizationName: string,
existingRisks: { title: string }[],
): Promise<RiskData[]> {
const result = await generateObject({
const { object } = await generateObject({
model: openai('gpt-4.1-mini'),
schema: z.object({
risks: z.array(
z.object({
risk_name: z.string(),
risk_description: z.string(),
risk_treatment_strategy: z.enum(
Object.values(RiskTreatmentType) as [string, ...string[]],
),
risk_treatment_strategy_description: z.string(),
risk_residual_probability: z.enum(Object.values(Likelihood) as [string, ...string[]]),
risk_residual_impact: z.enum(Object.values(Impact) as [string, ...string[]]),
category: z.enum(Object.values(RiskCategory) as [string, ...string[]]),
department: z.enum(Object.values(Departments) as [string, ...string[]]),
}),
),
mode: 'json',
schema: jsonSchema({
type: 'object',
properties: {
risks: {
type: 'array',
items: {
type: 'object',
properties: {
risk_name: { type: 'string' },
risk_description: { type: 'string' },
risk_treatment_strategy: { type: 'string', enum: Object.values(RiskTreatmentType) },
risk_treatment_strategy_description: { type: 'string' },
risk_residual_probability: { type: 'string', enum: Object.values(Likelihood) },
risk_residual_impact: { type: 'string', enum: Object.values(Impact) },
category: { type: 'string', enum: Object.values(RiskCategory) },
department: { type: 'string', enum: Object.values(Departments) },
},
required: [
'risk_name',
'risk_description',
'risk_treatment_strategy',
'risk_treatment_strategy_description',
'risk_residual_probability',
'risk_residual_impact',
'category',
'department',
],
},
},
},
required: ['risks'],
}),
system: `Create a list of 8-12 risks that are relevant to the organization. Use action-oriented language, assume reviewers understand basic termilology - skip definitions.
Your mandate is to propose risks that satisfy both ISO 27001:2022 clause 6.1 (risk management) and SOC 2 trust services criteria CC3 and CC4.
Expand All @@ -367,7 +455,7 @@ export async function extractRisksFromContext(
`,
});

return result.object.risks as RiskData[];
return (object as { risks: RiskData[] }).risks;
}

/**
Expand Down Expand Up @@ -489,7 +577,10 @@ export async function createRisks(
organizationId: string,
organizationName: string,
): Promise<Risk[]> {
// Get existing risks to avoid duplicates
// Ensure baseline risks exist first so the AI doesn't recreate them
await ensureBaselineRisks(organizationId);

// Get existing risks to avoid duplicates (includes baseline)
const existingRisks = await getExistingRisks(organizationId);

// Extract risks using AI
Expand Down
Loading
Loading