Skip to content

Commit fee8f14

Browse files
committed
feat(api): update firecrawl integration and enhance risk assessment task
1 parent 72e4a47 commit fee8f14

23 files changed

+1507
-176
lines changed

apps/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@browserbasehq/sdk": "^2.6.0",
1515
"@browserbasehq/stagehand": "^3.0.5",
1616
"@comp/integration-platform": "workspace:*",
17+
"@mendable/firecrawl-js": "^4.9.3",
1718
"@nestjs/common": "^11.0.1",
1819
"@nestjs/config": "^4.0.2",
1920
"@nestjs/core": "^11.0.1",

apps/api/src/trigger/vendor/vendor-risk-assessment-task.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
VENDOR_RISK_ASSESSMENT_TASK_TITLE,
88
} from './vendor-risk-assessment/constants';
99
import { buildRiskAssessmentDescription } from './vendor-risk-assessment/description';
10-
import { firecrawlExtractVendorData } from './vendor-risk-assessment/firecrawl';
10+
import { firecrawlAgentVendorRiskAssessment } from './vendor-risk-assessment/firecrawl-agent';
1111
import {
1212
buildFrameworkChecklist,
1313
getDefaultFrameworks,
@@ -107,7 +107,10 @@ export const vendorRiskAssessmentTask = schemaTask({
107107

108108
const research =
109109
payload.withResearch && payload.vendorWebsite
110-
? await firecrawlExtractVendorData(payload.vendorWebsite)
110+
? await firecrawlAgentVendorRiskAssessment({
111+
vendorName: payload.vendorName,
112+
vendorWebsite: payload.vendorWebsite,
113+
})
111114
: null;
112115

113116
const organizationFrameworks = getDefaultFrameworks();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { z } from 'zod';
2+
3+
const urlOrEmptySchema = z.union([z.string().url(), z.literal('')]).optional().nullable();
4+
// Firecrawl may return various date formats (ISO, "YYYY-MM-DD", etc). We normalize later.
5+
const dateStringOrEmptySchema = z.union([z.string(), z.literal('')]).optional().nullable();
6+
7+
export const vendorRiskAssessmentAgentSchema = z.object({
8+
risk_level: z.string().optional().nullable(),
9+
security_assessment: z.string().optional().nullable(),
10+
last_researched_at: dateStringOrEmptySchema,
11+
certifications: z
12+
.array(
13+
z.object({
14+
type: z.string(),
15+
status: z.enum(['verified', 'expired', 'not_certified', 'unknown']).optional().nullable(),
16+
issued_at: dateStringOrEmptySchema,
17+
expires_at: dateStringOrEmptySchema,
18+
url: urlOrEmptySchema,
19+
}),
20+
)
21+
.optional()
22+
.nullable(),
23+
links: z
24+
.object({
25+
privacy_policy_url: urlOrEmptySchema,
26+
terms_of_service_url: urlOrEmptySchema,
27+
trust_center_url: urlOrEmptySchema,
28+
security_page_url: urlOrEmptySchema,
29+
soc2_report_url: urlOrEmptySchema,
30+
})
31+
.optional()
32+
.nullable(),
33+
news: z
34+
.array(
35+
z.object({
36+
date: z.string(),
37+
title: z.string(),
38+
summary: z.string().optional().nullable(),
39+
source: z.string().optional().nullable(),
40+
url: urlOrEmptySchema,
41+
sentiment: z.enum(['positive', 'negative', 'neutral']).optional().nullable(),
42+
}),
43+
)
44+
.optional()
45+
.nullable(),
46+
});
47+
48+
export type VendorRiskAssessmentAgentResult = z.infer<
49+
typeof vendorRiskAssessmentAgentSchema
50+
>;
51+
52+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type VendorRiskAssessmentCertificationStatus =
2+
| 'verified'
3+
| 'expired'
4+
| 'not_certified'
5+
| 'unknown';
6+
7+
export type VendorRiskAssessmentCertification = {
8+
type: string;
9+
status: VendorRiskAssessmentCertificationStatus;
10+
issuedAt?: string | null;
11+
expiresAt?: string | null;
12+
url?: string | null;
13+
};
14+
15+
export type VendorRiskAssessmentLink = {
16+
label: string;
17+
url: string;
18+
};
19+
20+
export type VendorRiskAssessmentNewsSentiment = 'positive' | 'negative' | 'neutral';
21+
22+
export type VendorRiskAssessmentNewsItem = {
23+
date: string;
24+
title: string;
25+
summary?: string | null;
26+
source?: string | null;
27+
url?: string | null;
28+
sentiment?: VendorRiskAssessmentNewsSentiment | null;
29+
};
30+
31+
export type VendorRiskAssessmentDataV1 = {
32+
kind: 'vendorRiskAssessmentV1';
33+
vendorName?: string | null;
34+
vendorWebsite?: string | null;
35+
lastResearchedAt?: string | null;
36+
riskLevel?: string | null;
37+
securityAssessment?: string | null;
38+
certifications?: VendorRiskAssessmentCertification[] | null;
39+
links?: VendorRiskAssessmentLink[] | null;
40+
news?: VendorRiskAssessmentNewsItem[] | null;
41+
};
42+
43+
Lines changed: 28 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,39 @@
11
import type { OrgFramework } from './frameworks';
2-
import type { FirecrawlVendorData } from './schema';
2+
import type { VendorRiskAssessmentDataV1 } from './agent-types';
33

44
export function buildRiskAssessmentDescription(params: {
55
vendorName: string;
66
vendorWebsite: string | null;
7-
research: FirecrawlVendorData | null;
7+
research: VendorRiskAssessmentDataV1 | null;
88
frameworkChecklist: string[];
99
organizationFrameworks: OrgFramework[];
1010
}): string {
11-
const { vendorName, vendorWebsite, research, frameworkChecklist, organizationFrameworks } =
12-
params;
13-
14-
const instruction =
15-
'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.';
16-
17-
const links: Array<{ label: string; url: string }> = [];
18-
if (research?.trust_portal_url)
19-
links.push({ label: 'Trust Center', url: research.trust_portal_url });
20-
if (research?.security_overview_url)
21-
links.push({ label: 'Security Overview', url: research.security_overview_url });
22-
if (research?.soc2_report_url)
23-
links.push({ label: 'SOC 2 Report', url: research.soc2_report_url });
24-
if (research?.privacy_policy_url)
25-
links.push({ label: 'Privacy Policy', url: research.privacy_policy_url });
26-
if (research?.terms_of_service_url)
27-
links.push({ label: 'Terms of Service', url: research.terms_of_service_url });
28-
29-
const content: Array<Record<string, unknown>> = [
30-
{
31-
type: 'paragraph',
32-
content: [{ type: 'text', text: instruction }],
33-
},
34-
{
35-
type: 'paragraph',
36-
content: [
37-
{ type: 'text', marks: [{ type: 'bold' }], text: 'Vendor:' },
38-
{ type: 'text', text: ` ${vendorName}` },
39-
],
40-
},
41-
];
42-
43-
if (vendorWebsite) {
44-
content.push({
45-
type: 'paragraph',
46-
content: [
47-
{ type: 'text', marks: [{ type: 'bold' }], text: 'Website:' },
48-
{ type: 'text', text: ` ${vendorWebsite}` },
49-
],
50-
});
51-
}
52-
53-
// Intentionally omit "Framework Focus" line to keep the description concise.
54-
55-
if (frameworkChecklist.length > 0) {
56-
content.push({
57-
type: 'paragraph',
58-
content: [
59-
{ type: 'text', marks: [{ type: 'bold' }], text: 'Framework-specific checks:' },
60-
],
61-
});
62-
content.push({
63-
type: 'bulletList',
64-
content: frameworkChecklist.map((item) => ({
65-
type: 'listItem',
66-
content: [{ type: 'paragraph', content: [{ type: 'text', text: item }] }],
67-
})),
68-
});
69-
}
70-
71-
if (research?.company_description) {
72-
content.push({
73-
type: 'paragraph',
74-
content: [
75-
{ type: 'text', marks: [{ type: 'bold' }], text: 'Company Overview:' },
76-
],
77-
});
78-
content.push({
79-
type: 'paragraph',
80-
content: [{ type: 'text', text: research.company_description }],
81-
});
82-
}
83-
84-
const certs = research?.certified_security_frameworks?.filter(Boolean) ?? [];
85-
if (certs.length > 0) {
86-
content.push({
87-
type: 'paragraph',
88-
content: [
89-
{ type: 'text', marks: [{ type: 'bold' }], text: 'Security Certifications:' },
90-
],
91-
});
92-
content.push({
93-
type: 'bulletList',
94-
content: certs.map((framework) => ({
95-
type: 'listItem',
96-
content: [
97-
{ type: 'paragraph', content: [{ type: 'text', text: framework }] },
98-
],
99-
})),
100-
});
101-
}
102-
103-
if (links.length > 0) {
104-
content.push({
105-
type: 'paragraph',
106-
content: [
107-
{ type: 'text', marks: [{ type: 'bold' }], text: 'Relevant Links:' },
108-
],
109-
});
110-
content.push({
111-
type: 'bulletList',
112-
content: links.map((link) => ({
113-
type: 'listItem',
114-
content: [
115-
{
116-
type: 'paragraph',
117-
content: [
118-
{
119-
type: 'text',
120-
marks: [
121-
{
122-
type: 'link',
123-
attrs: {
124-
href: link.url,
125-
target: '_blank',
126-
rel: 'noopener noreferrer',
127-
},
128-
},
129-
],
130-
text: link.label,
131-
},
132-
],
133-
},
134-
],
135-
})),
136-
});
137-
} else if (vendorWebsite) {
138-
content.push({
139-
type: 'paragraph',
140-
content: [
141-
{
142-
type: 'text',
143-
marks: [{ type: 'italic' }],
144-
text: 'Note: Automated research did not return links. Please collect documentation manually.',
145-
},
146-
],
147-
});
148-
} else {
149-
content.push({
150-
type: 'paragraph',
151-
content: [
152-
{
153-
type: 'text',
154-
marks: [{ type: 'italic' }],
155-
text: 'Note: No website provided for automated research.',
156-
},
157-
],
158-
});
159-
}
160-
161-
return JSON.stringify({ type: 'doc', content });
11+
const { vendorName, vendorWebsite, research, frameworkChecklist } = params;
12+
13+
const base: VendorRiskAssessmentDataV1 = research ?? {
14+
kind: 'vendorRiskAssessmentV1',
15+
vendorName,
16+
vendorWebsite,
17+
lastResearchedAt: null,
18+
riskLevel: null,
19+
securityAssessment: null,
20+
certifications: null,
21+
links: null,
22+
news: null,
23+
};
24+
25+
// Keep the existing “framework checklist” value for humans (rendered inside the Security Assessment card).
26+
const checklistSuffix =
27+
frameworkChecklist.length > 0
28+
? `\n\nFramework-specific checks:\n${frameworkChecklist.map((c) => `- ${c}`).join('\n')}`
29+
: '';
30+
31+
return JSON.stringify({
32+
...base,
33+
vendorName: base.vendorName ?? vendorName,
34+
vendorWebsite: base.vendorWebsite ?? vendorWebsite,
35+
securityAssessment: (base.securityAssessment ?? '') + checklistSuffix || null,
36+
} satisfies VendorRiskAssessmentDataV1);
16237
}
16338

16439

0 commit comments

Comments
 (0)