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
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@browserbasehq/sdk": "^2.6.0",
"@browserbasehq/stagehand": "^3.0.5",
"@comp/integration-platform": "workspace:*",
"@mendable/firecrawl-js": "^4.9.3",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
Expand Down
7 changes: 5 additions & 2 deletions apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
VENDOR_RISK_ASSESSMENT_TASK_TITLE,
} from './vendor-risk-assessment/constants';
import { buildRiskAssessmentDescription } from './vendor-risk-assessment/description';
import { firecrawlExtractVendorData } from './vendor-risk-assessment/firecrawl';
import { firecrawlAgentVendorRiskAssessment } from './vendor-risk-assessment/firecrawl-agent';
import {
buildFrameworkChecklist,
getDefaultFrameworks,
Expand Down Expand Up @@ -107,7 +107,10 @@ export const vendorRiskAssessmentTask = schemaTask({

const research =
payload.withResearch && payload.vendorWebsite
? await firecrawlExtractVendorData(payload.vendorWebsite)
? await firecrawlAgentVendorRiskAssessment({
vendorName: payload.vendorName,
vendorWebsite: payload.vendorWebsite,
})
: null;

const organizationFrameworks = getDefaultFrameworks();
Expand Down
52 changes: 52 additions & 0 deletions apps/api/src/trigger/vendor/vendor-risk-assessment/agent-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { z } from 'zod';

const urlOrEmptySchema = z.union([z.string().url(), z.literal('')]).optional().nullable();
// Firecrawl may return various date formats (ISO, "YYYY-MM-DD", etc). We normalize later.
const dateStringOrEmptySchema = z.union([z.string(), z.literal('')]).optional().nullable();

export const vendorRiskAssessmentAgentSchema = z.object({
risk_level: z.string().optional().nullable(),
security_assessment: z.string().optional().nullable(),
last_researched_at: dateStringOrEmptySchema,
certifications: z
.array(
z.object({
type: z.string(),
status: z.enum(['verified', 'expired', 'not_certified', 'unknown']).optional().nullable(),
issued_at: dateStringOrEmptySchema,
expires_at: dateStringOrEmptySchema,
url: urlOrEmptySchema,
}),
)
.optional()
.nullable(),
links: z
.object({
privacy_policy_url: urlOrEmptySchema,
terms_of_service_url: urlOrEmptySchema,
trust_center_url: urlOrEmptySchema,
security_page_url: urlOrEmptySchema,
soc2_report_url: urlOrEmptySchema,
})
.optional()
.nullable(),
news: z
.array(
z.object({
date: z.string(),
title: z.string(),
summary: z.string().optional().nullable(),
source: z.string().optional().nullable(),
url: urlOrEmptySchema,
sentiment: z.enum(['positive', 'negative', 'neutral']).optional().nullable(),
}),
)
.optional()
.nullable(),
});

export type VendorRiskAssessmentAgentResult = z.infer<
typeof vendorRiskAssessmentAgentSchema
>;


43 changes: 43 additions & 0 deletions apps/api/src/trigger/vendor/vendor-risk-assessment/agent-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export type VendorRiskAssessmentCertificationStatus =
| 'verified'
| 'expired'
| 'not_certified'
| 'unknown';

export type VendorRiskAssessmentCertification = {
type: string;
status: VendorRiskAssessmentCertificationStatus;
issuedAt?: string | null;
expiresAt?: string | null;
url?: string | null;
};

export type VendorRiskAssessmentLink = {
label: string;
url: string;
};

export type VendorRiskAssessmentNewsSentiment = 'positive' | 'negative' | 'neutral';

export type VendorRiskAssessmentNewsItem = {
date: string;
title: string;
summary?: string | null;
source?: string | null;
url?: string | null;
sentiment?: VendorRiskAssessmentNewsSentiment | null;
};

export type VendorRiskAssessmentDataV1 = {
kind: 'vendorRiskAssessmentV1';
vendorName?: string | null;
vendorWebsite?: string | null;
lastResearchedAt?: string | null;
riskLevel?: string | null;
securityAssessment?: string | null;
certifications?: VendorRiskAssessmentCertification[] | null;
links?: VendorRiskAssessmentLink[] | null;
news?: VendorRiskAssessmentNewsItem[] | null;
};


181 changes: 28 additions & 153 deletions apps/api/src/trigger/vendor/vendor-risk-assessment/description.ts
Original file line number Diff line number Diff line change
@@ -1,164 +1,39 @@
import type { OrgFramework } from './frameworks';
import type { FirecrawlVendorData } from './schema';
import type { VendorRiskAssessmentDataV1 } from './agent-types';

export function buildRiskAssessmentDescription(params: {
vendorName: string;
vendorWebsite: string | null;
research: FirecrawlVendorData | null;
research: VendorRiskAssessmentDataV1 | null;
frameworkChecklist: string[];
organizationFrameworks: OrgFramework[];
}): string {
const { vendorName, vendorWebsite, research, frameworkChecklist, organizationFrameworks } =
params;

const instruction =
'Conduct a risk assessment for this vendor. Review their controls and documentation against SOC 2 and ISO 27001 expectations and add a comment describing how your team will use the vendor securely.';

const links: Array<{ label: string; url: string }> = [];
if (research?.trust_portal_url)
links.push({ label: 'Trust Center', url: research.trust_portal_url });
if (research?.security_overview_url)
links.push({ label: 'Security Overview', url: research.security_overview_url });
if (research?.soc2_report_url)
links.push({ label: 'SOC 2 Report', url: research.soc2_report_url });
if (research?.privacy_policy_url)
links.push({ label: 'Privacy Policy', url: research.privacy_policy_url });
if (research?.terms_of_service_url)
links.push({ label: 'Terms of Service', url: research.terms_of_service_url });

const content: Array<Record<string, unknown>> = [
{
type: 'paragraph',
content: [{ type: 'text', text: instruction }],
},
{
type: 'paragraph',
content: [
{ type: 'text', marks: [{ type: 'bold' }], text: 'Vendor:' },
{ type: 'text', text: ` ${vendorName}` },
],
},
];

if (vendorWebsite) {
content.push({
type: 'paragraph',
content: [
{ type: 'text', marks: [{ type: 'bold' }], text: 'Website:' },
{ type: 'text', text: ` ${vendorWebsite}` },
],
});
}

// Intentionally omit "Framework Focus" line to keep the description concise.

if (frameworkChecklist.length > 0) {
content.push({
type: 'paragraph',
content: [
{ type: 'text', marks: [{ type: 'bold' }], text: 'Framework-specific checks:' },
],
});
content.push({
type: 'bulletList',
content: frameworkChecklist.map((item) => ({
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: item }] }],
})),
});
}

if (research?.company_description) {
content.push({
type: 'paragraph',
content: [
{ type: 'text', marks: [{ type: 'bold' }], text: 'Company Overview:' },
],
});
content.push({
type: 'paragraph',
content: [{ type: 'text', text: research.company_description }],
});
}

const certs = research?.certified_security_frameworks?.filter(Boolean) ?? [];
if (certs.length > 0) {
content.push({
type: 'paragraph',
content: [
{ type: 'text', marks: [{ type: 'bold' }], text: 'Security Certifications:' },
],
});
content.push({
type: 'bulletList',
content: certs.map((framework) => ({
type: 'listItem',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: framework }] },
],
})),
});
}

if (links.length > 0) {
content.push({
type: 'paragraph',
content: [
{ type: 'text', marks: [{ type: 'bold' }], text: 'Relevant Links:' },
],
});
content.push({
type: 'bulletList',
content: links.map((link) => ({
type: 'listItem',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: link.url,
target: '_blank',
rel: 'noopener noreferrer',
},
},
],
text: link.label,
},
],
},
],
})),
});
} else if (vendorWebsite) {
content.push({
type: 'paragraph',
content: [
{
type: 'text',
marks: [{ type: 'italic' }],
text: 'Note: Automated research did not return links. Please collect documentation manually.',
},
],
});
} else {
content.push({
type: 'paragraph',
content: [
{
type: 'text',
marks: [{ type: 'italic' }],
text: 'Note: No website provided for automated research.',
},
],
});
}

return JSON.stringify({ type: 'doc', content });
const { vendorName, vendorWebsite, research, frameworkChecklist } = params;

const base: VendorRiskAssessmentDataV1 = research ?? {
kind: 'vendorRiskAssessmentV1',
vendorName,
vendorWebsite,
lastResearchedAt: null,
riskLevel: null,
securityAssessment: null,
certifications: null,
links: null,
news: null,
};

// Keep the existing “framework checklist” value for humans (rendered inside the Security Assessment card).
const checklistSuffix =
frameworkChecklist.length > 0
? `\n\nFramework-specific checks:\n${frameworkChecklist.map((c) => `- ${c}`).join('\n')}`
: '';

return JSON.stringify({
...base,
vendorName: base.vendorName ?? vendorName,
vendorWebsite: base.vendorWebsite ?? vendorWebsite,
securityAssessment: (base.securityAssessment ?? '') + checklistSuffix || null,
} satisfies VendorRiskAssessmentDataV1);
}


Loading