Fetch vendor data with openai

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-05-26 10:45:10 -07:00
parent 8f41461bd1
commit c2f9cb2080
15 changed files with 1044 additions and 25 deletions

View File

@@ -20,6 +20,7 @@ import type { VendorViewDeleteComplianceReportMutation as DeleteComplianceReport
import type { VendorViewUploadComplianceReportMutation as UploadComplianceReportMutationType } from "./__generated__/VendorViewUploadComplianceReportMutation.graphql"; import type { VendorViewUploadComplianceReportMutation as UploadComplianceReportMutationType } from "./__generated__/VendorViewUploadComplianceReportMutation.graphql";
import type { VendorViewUpdateVendorMutation } from "./__generated__/VendorViewUpdateVendorMutation.graphql"; import type { VendorViewUpdateVendorMutation } from "./__generated__/VendorViewUpdateVendorMutation.graphql";
import type { VendorViewCreateRiskAssessmentMutation } from "./__generated__/VendorViewCreateRiskAssessmentMutation.graphql"; import type { VendorViewCreateRiskAssessmentMutation } from "./__generated__/VendorViewCreateRiskAssessmentMutation.graphql";
import type { VendorViewAssessVendorMutation } from "./__generated__/VendorViewAssessVendorMutation.graphql";
import type { import type {
BusinessImpact, BusinessImpact,
DataSensitivity, DataSensitivity,
@@ -31,6 +32,7 @@ import { PageTemplate } from "@/components/PageTemplate";
import { VendorViewSkeleton } from "./VendorPage"; import { VendorViewSkeleton } from "./VendorPage";
import PeopleSelector from "@/components/PeopleSelector"; import PeopleSelector from "@/components/PeopleSelector";
import { formatDistanceToNow, format, isPast } from "date-fns"; import { formatDistanceToNow, format, isPast } from "date-fns";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
const vendorViewQuery = graphql` const vendorViewQuery = graphql`
query VendorViewQuery($vendorId: ID!, $organizationId: ID!) { query VendorViewQuery($vendorId: ID!, $organizationId: ID!) {
@@ -196,6 +198,38 @@ const createRiskAssessmentMutation = graphql`
} }
`; `;
const assessVendorMutation = graphql`
mutation VendorViewAssessVendorMutation($input: AssessVendorInput!) {
assessVendor(input: $input) {
vendor {
id
name
description
statusPageUrl
termsOfServiceUrl
privacyPolicyUrl
serviceLevelAgreementUrl
dataProcessingAgreementUrl
securityPageUrl
trustPageUrl
certifications
headquarterAddress
legalName
websiteUrl
businessOwner {
id
fullName
}
securityOwner {
id
fullName
}
updatedAt
}
}
}
`;
// Format date for input field (YYYY-MM-DDTHH:mm) // Format date for input field (YYYY-MM-DDTHH:mm)
function formatDateForInput(date: string | null | undefined): string { function formatDateForInput(date: string | null | undefined): string {
if (!date) return ""; if (!date) return "";
@@ -1398,6 +1432,13 @@ function VendorViewContent({
); );
const [, loadQuery] = useQueryLoader<VendorViewQueryType>(vendorViewQuery); const [, loadQuery] = useQueryLoader<VendorViewQueryType>(vendorViewQuery);
const { toast } = useToast(); const { toast } = useToast();
const [isAssessDialogOpen, setIsAssessDialogOpen] = useState(false);
const [websiteUrl, setWebsiteUrl] = useState("");
const [commitAssessVendor, isInFlight] = useMutation<VendorViewAssessVendorMutation>(
assessVendorMutation
);
const hasChanges = editedFields.size > 0; const hasChanges = editedFields.size > 0;
@@ -2301,8 +2342,92 @@ function VendorViewContent({
} }
}; };
const handleAssessVendor = useCallback(() => {
if (!websiteUrl) {
toast({
title: "Error",
description: "Please enter a website URL",
variant: "destructive",
});
return;
}
commitAssessVendor({
variables: {
input: {
id: data.node.id!,
websiteUrl,
},
},
onCompleted: (response) => {
const newData = response.assessVendor.vendor;
const changedFields = new Set<string>();
// Compare and track changes
Object.entries(newData).forEach(([key, value]) => {
if (value != null && key !== 'id' && formData[key as keyof typeof formData] !== value) {
changedFields.add(key);
}
});
setFormData(prevData => {
const newFormData = {
...prevData,
name: newData.name || prevData.name,
description: newData.description || prevData.description,
statusPageUrl: newData.statusPageUrl || prevData.statusPageUrl,
termsOfServiceUrl: newData.termsOfServiceUrl || prevData.termsOfServiceUrl,
privacyPolicyUrl: newData.privacyPolicyUrl || prevData.privacyPolicyUrl,
serviceLevelAgreementUrl: newData.serviceLevelAgreementUrl || prevData.serviceLevelAgreementUrl,
dataProcessingAgreementUrl: newData.dataProcessingAgreementUrl || prevData.dataProcessingAgreementUrl,
securityPageUrl: newData.securityPageUrl || prevData.securityPageUrl,
trustPageUrl: newData.trustPageUrl || prevData.trustPageUrl,
certifications: newData.certifications || prevData.certifications,
headquarterAddress: newData.headquarterAddress || prevData.headquarterAddress,
legalName: newData.legalName || prevData.legalName,
websiteUrl: newData.websiteUrl || prevData.websiteUrl,
};
return newFormData;
});
setEditedFields(prev => {
const newSet = new Set(prev);
changedFields.forEach(field => newSet.add(field));
return newSet;
});
toast({
title: "Success",
description: changedFields.size > 0
? "Vendor has been assessed. Review and save the changes."
: "Vendor data has been assessed. No changes were found.",
});
setIsAssessDialogOpen(false);
setWebsiteUrl("");
},
onError: (error) => {
toast({
title: "Error",
description: error.message,
variant: "destructive",
});
},
});
}, [commitAssessVendor, data.node.id, websiteUrl, toast, formData]);
return ( return (
<PageTemplate title={formData.name}> <PageTemplate
title={formData.name}
actions={
<Button
className="rounded-full bg-[rgba(0,39,0,0.05)] text-[#141E12] hover:bg-[rgba(0,39,0,0.08)] h-8 px-3"
size="sm"
onClick={() => setIsAssessDialogOpen(true)}
>
Assessement From Website
</Button>
}
>
<div className="border-b mb-6"> <div className="border-b mb-6">
<div className="flex"> <div className="flex">
<button <button
@@ -2377,12 +2502,71 @@ function VendorViewContent({
onSubmit={handleRiskAssessmentSubmit} onSubmit={handleRiskAssessmentSubmit}
businessOwnerId={formData.businessOwnerId} businessOwnerId={formData.businessOwnerId}
/> />
{/* Assessement From Website Modal */}
<Dialog open={isAssessDialogOpen} onOpenChange={setIsAssessDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Assessement From Website</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="Enter website URL"
value={websiteUrl}
onChange={(e) => setWebsiteUrl(e.target.value)}
disabled={isInFlight}
/>
{isInFlight && (
<div className="flex items-center justify-center text-sm text-[#6B716A]">
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-[#054D05]"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Assessing vendor data...
</div>
)}
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setIsAssessDialogOpen(false)}
disabled={isInFlight}
>
Cancel
</Button>
<Button
onClick={handleAssessVendor}
disabled={isInFlight}
className={isInFlight ? "opacity-50 cursor-not-allowed" : ""}
>
{isInFlight ? "Processing..." : "Assess"}
</Button>
</div>
</DialogContent>
</Dialog>
</PageTemplate> </PageTemplate>
); );
} }
export default function VendorView() { export default function VendorView() {
const { vendorId, organizationId } = useParams(); const { vendorId, organizationId } = useParams();
const [queryRef, loadQuery] = const [queryRef, loadQuery] =
useQueryLoader<VendorViewQueryType>(vendorViewQuery); useQueryLoader<VendorViewQueryType>(vendorViewQuery);

View File

@@ -0,0 +1,257 @@
/**
* @generated SignedSource<<71c7ed1be8cf0477074d6cf130143626>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type AssessVendorInput = {
id: string;
websiteUrl: string;
};
export type VendorViewAssessVendorMutation$variables = {
input: AssessVendorInput;
};
export type VendorViewAssessVendorMutation$data = {
readonly assessVendor: {
readonly vendor: {
readonly businessOwner: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly certifications: ReadonlyArray<string>;
readonly dataProcessingAgreementUrl: string | null | undefined;
readonly description: string | null | undefined;
readonly headquarterAddress: string | null | undefined;
readonly id: string;
readonly legalName: string | null | undefined;
readonly name: string;
readonly privacyPolicyUrl: string | null | undefined;
readonly securityOwner: {
readonly fullName: string;
readonly id: string;
} | null | undefined;
readonly securityPageUrl: string | null | undefined;
readonly serviceLevelAgreementUrl: string | null | undefined;
readonly statusPageUrl: string | null | undefined;
readonly termsOfServiceUrl: string | null | undefined;
readonly trustPageUrl: string | null | undefined;
readonly updatedAt: string;
readonly websiteUrl: string | null | undefined;
};
};
};
export type VendorViewAssessVendorMutation = {
response: VendorViewAssessVendorMutation$data;
variables: VendorViewAssessVendorMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v2 = [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
}
],
v3 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "AssessVendorPayload",
"kind": "LinkedField",
"name": "assessVendor",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "vendor",
"plural": false,
"selections": [
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "statusPageUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "termsOfServiceUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "serviceLevelAgreementUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "dataProcessingAgreementUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "securityPageUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "trustPageUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "certifications",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "legalName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "businessOwner",
"plural": false,
"selections": (v2/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "People",
"kind": "LinkedField",
"name": "securityOwner",
"plural": false,
"selections": (v2/*: any*/),
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "VendorViewAssessVendorMutation",
"selections": (v3/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "VendorViewAssessVendorMutation",
"selections": (v3/*: any*/)
},
"params": {
"cacheID": "ccc16b2de8f7f3936458cd4823a14992",
"id": null,
"metadata": {},
"name": "VendorViewAssessVendorMutation",
"operationKind": "mutation",
"text": "mutation VendorViewAssessVendorMutation(\n $input: AssessVendorInput!\n) {\n assessVendor(input: $input) {\n vendor {\n id\n name\n description\n statusPageUrl\n termsOfServiceUrl\n privacyPolicyUrl\n serviceLevelAgreementUrl\n dataProcessingAgreementUrl\n securityPageUrl\n trustPageUrl\n certifications\n headquarterAddress\n legalName\n websiteUrl\n businessOwner {\n id\n fullName\n }\n securityOwner {\n id\n fullName\n }\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ed23d00ce5a44aa6bfd43a072552ed1c";
export default node;

View File

@@ -6,3 +6,7 @@ probod:
access-key-id: "probod" access-key-id: "probod"
secret-access-key: "thisisnotasecret" secret-access-key: "thisisnotasecret"
endpoint: "http://127.0.0.1:9000" endpoint: "http://127.0.0.1:9000"
openai:
api-key: "thisisnotasecret"
temperature: 0.1
model-name: "gpt-4o"

5
go.mod
View File

@@ -16,6 +16,7 @@ require (
github.com/go-chi/cors v1.2.1 github.com/go-chi/cors v1.2.1
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.7.4
github.com/jhillyerd/enmime v1.3.0 github.com/jhillyerd/enmime v1.3.0
github.com/openai/openai-go v1.1.0
github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_golang v1.22.0
github.com/vektah/gqlparser/v2 v2.5.27 github.com/vektah/gqlparser/v2 v2.5.27
go.gearno.de/crypto/uuid v0.1.0 go.gearno.de/crypto/uuid v0.1.0
@@ -64,6 +65,10 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect github.com/sosodev/duration v1.3.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/urfave/cli/v2 v2.27.6 // indirect github.com/urfave/cli/v2 v2.27.6 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.gearno.de/x/panicf v0.1.1 // indirect go.gearno.de/x/panicf v0.1.1 // indirect

12
go.sum
View File

@@ -108,6 +108,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/openai/openai-go v1.1.0 h1:daSn+y+3QJUmLV1xfh7B8QtgJYRw1hg3yWxKtQDfROE=
github.com/openai/openai-go v1.1.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -140,6 +142,16 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package agents
import (
"context"
"encoding/json"
"fmt"
"github.com/openai/openai-go"
"github.com/openai/openai-go/option"
"github.com/openai/openai-go/packages/param"
"go.gearno.de/kit/log"
)
type (
VendorAssessment struct {
l *log.Logger
cfg Config
client *openai.Client
}
Config struct {
OpenAIAPIKey string
Temperature float64
ModelName string
}
vendorInfo struct {
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
HeadquarterAddress string `json:"headquarter_address"`
LegalName string `json:"legal_name"`
PrivacyPolicyURL string `json:"privacy_policy_url"`
ServiceLevelAgreementURL string `json:"service_level_agreement_url"`
DataProcessingAgreementURL string `json:"data_processing_agreement_url"`
SecurityPageURL string `json:"security_page_url"`
TrustPageURL string `json:"trust_page_url"`
TermsOfServiceURL string `json:"terms_of_service_url"`
StatusPageURL string `json:"status_page_url"`
Certifications []string `json:"certifications"`
}
)
const (
systemPrompt = `
# Role: You are a compliance assistant.
# Objective
Your task is to fetch the provided company URL and to return comprehensive company information.
# For the company url, return the following fields in structured JSON format:
- name: The company's commonly used name
- description: One-sentence summary of the company's core offering
- headquarter_address: Company's main headquarter full address
- legal_name: Official registered company name
- privacy_policy_url: URL to privacy policy page
- service_level_agreement_url: URL to SLA page
- data_processing_agreement_url: URL to DPA page
- security_page_url: URL to security information page
- trust_page_url: URL to trust/compliance page
- terms_of_service_url: URL to terms of service page
- status_page_url: URL to system status page
- certifications: Array of security/compliance certifications (e.g., ["SOC2", "ISO27001"])
# SOP
- Please ensure the output is clean, standardized JSON.
- Use web search to gather info, if you cannot find what you are looking for, just return an empty string instead
- For URLs, return the full URL if found, otherwise an empty string
- For certifications, return an empty array if none found
# **Example output format:**
Respond ONLY with a JSON object. No explanation, no markdown, no preamble. Like this:
{
"name": "Stripe",
"description": "Online payment processing platform that enables businesses to accept and manage digital payments, supporting various payment methods and currencies with integrated fraud protection and compliance features",
"headquarter_address": "San Francisco, CA",
"legal_name": "Stripe, Inc.",
"privacy_policy_url": "https://stripe.com/privacy",
"service_level_agreement_url": "https://stripe.com/sla",
"data_processing_agreement_url": "https://stripe.com/dpa",
"security_page_url": "https://stripe.com/security",
"trust_page_url": "https://stripe.com/trust",
"terms_of_service_url": "https://stripe.com/terms",
"status_page_url": "https://status.stripe.com",
"certifications": ["SOC1", "SOC2", "PCI DSS Level 1", "ISO 27001"]
}
### Company url:
`
)
func NewVendorAssessment(l *log.Logger, cfg Config) *VendorAssessment {
client := openai.NewClient(option.WithAPIKey(cfg.OpenAIAPIKey))
return &VendorAssessment{l: l, cfg: cfg, client: &client}
}
func (va *VendorAssessment) Fetch(ctx context.Context, websiteURL string) (*vendorInfo, error) {
model := openai.ChatModel(va.cfg.ModelName)
chatCompletion, err := va.client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
Messages: []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage(systemPrompt),
openai.UserMessage(websiteURL),
},
Model: model,
Temperature: param.NewOpt(va.cfg.Temperature),
})
if err != nil {
return nil, fmt.Errorf("failed to parse vendor info: %w", err)
}
if len(chatCompletion.Choices) == 0 {
return nil, fmt.Errorf("no completion choices returned from API")
}
var vendorInfo vendorInfo
err = json.Unmarshal([]byte(chatCompletion.Choices[0].Message.Content), &vendorInfo)
if err != nil {
return nil, fmt.Errorf("failed to parse vendor info: %w", err)
}
return &vendorInfo, nil
}

View File

@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/agents"
"github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/crypto/cipher" "github.com/getprobo/probo/pkg/crypto/cipher"
"github.com/getprobo/probo/pkg/filevalidation" "github.com/getprobo/probo/pkg/filevalidation"
@@ -34,6 +35,7 @@ type (
encryptionKey cipher.EncryptionKey encryptionKey cipher.EncryptionKey
hostname string hostname string
tokenSecret string tokenSecret string
vendorAssessment agents.Config
} }
TenantService struct { TenantService struct {
@@ -44,6 +46,7 @@ type (
scope coredata.Scoper scope coredata.Scoper
hostname string hostname string
tokenSecret string tokenSecret string
vendorAssessment *agents.VendorAssessment
Frameworks *FrameworkService Frameworks *FrameworkService
Measures *MeasureService Measures *MeasureService
Tasks *TaskService Tasks *TaskService
@@ -67,6 +70,7 @@ func NewService(
bucket string, bucket string,
hostname string, hostname string,
tokenSecret string, tokenSecret string,
vendorAssessment agents.Config,
) (*Service, error) { ) (*Service, error) {
if bucket == "" { if bucket == "" {
return nil, fmt.Errorf("bucket is required") return nil, fmt.Errorf("bucket is required")
@@ -79,6 +83,7 @@ func NewService(
encryptionKey: encryptionKey, encryptionKey: encryptionKey,
hostname: hostname, hostname: hostname,
tokenSecret: tokenSecret, tokenSecret: tokenSecret,
vendorAssessment: vendorAssessment,
} }
return svc, nil return svc, nil
@@ -93,6 +98,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
hostname: s.hostname, hostname: s.hostname,
scope: coredata.NewScope(tenantID), scope: coredata.NewScope(tenantID),
tokenSecret: s.tokenSecret, tokenSecret: s.tokenSecret,
vendorAssessment: agents.NewVendorAssessment(nil, s.vendorAssessment),
} }
tenantService.Frameworks = &FrameworkService{svc: tenantService} tenantService.Frameworks = &FrameworkService{svc: tenantService}

View File

@@ -70,6 +70,11 @@ type (
SecurityOwnerID *gid.GID SecurityOwnerID *gid.GID
} }
AssessVendorRequest struct {
ID gid.GID
WebsiteURL string
}
CreateVendorRiskAssessmentRequest struct { CreateVendorRiskAssessmentRequest struct {
VendorID gid.GID VendorID gid.GID
AssessedByID gid.GID AssessedByID gid.GID
@@ -421,3 +426,34 @@ func (s VendorService) GetRiskAssessment(
return vendorRiskAssessment, nil return vendorRiskAssessment, nil
} }
func (s VendorService) Assess(
ctx context.Context,
req AssessVendorRequest,
) (*coredata.Vendor, error) {
vendorInfo, err := s.svc.vendorAssessment.Fetch(ctx, req.WebsiteURL)
if err != nil {
return nil, fmt.Errorf("failed to assess vendor info: %w", err)
}
vendor := &coredata.Vendor{
ID: req.ID,
Name: vendorInfo.Name,
WebsiteURL: &req.WebsiteURL,
Description: &vendorInfo.Description,
Category: vendorInfo.Category,
HeadquarterAddress: &vendorInfo.HeadquarterAddress,
LegalName: &vendorInfo.LegalName,
PrivacyPolicyURL: &vendorInfo.PrivacyPolicyURL,
ServiceLevelAgreementURL: &vendorInfo.ServiceLevelAgreementURL,
DataProcessingAgreementURL: &vendorInfo.DataProcessingAgreementURL,
SecurityPageURL: &vendorInfo.SecurityPageURL,
TrustPageURL: &vendorInfo.TrustPageURL,
TermsOfServiceURL: &vendorInfo.TermsOfServiceURL,
StatusPageURL: &vendorInfo.StatusPageURL,
Certifications: vendorInfo.Certifications,
UpdatedAt: time.Now(),
}
return vendor, nil
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package probod
type openaiConfig struct {
APIKey string `json:"api-key"`
Temperature float64 `json:"temperature"`
ModelName string `json:"model-name"`
}

View File

@@ -24,6 +24,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/agents"
"github.com/getprobo/probo/pkg/awsconfig" "github.com/getprobo/probo/pkg/awsconfig"
"github.com/getprobo/probo/pkg/connector" "github.com/getprobo/probo/pkg/connector"
"github.com/getprobo/probo/pkg/coredata" "github.com/getprobo/probo/pkg/coredata"
@@ -59,6 +60,7 @@ type (
AWS awsConfig `json:"aws"` AWS awsConfig `json:"aws"`
Mailer mailerConfig `json:"mailer"` Mailer mailerConfig `json:"mailer"`
Connectors []connectorConfig `json:"connectors"` Connectors []connectorConfig `json:"connectors"`
OpenAI openaiConfig `json:"openai"`
} }
) )
@@ -185,6 +187,14 @@ func (impl *Implm) Run(
} }
} }
vendorAssessmentConfig := agents.Config{
OpenAIAPIKey: impl.cfg.OpenAI.APIKey,
Temperature: impl.cfg.OpenAI.Temperature,
ModelName: impl.cfg.OpenAI.ModelName,
}
vendorAssessment := agents.NewVendorAssessment(l.Named("vendor-assessment"), vendorAssessmentConfig)
usrmgrService, err := usrmgr.NewService( usrmgrService, err := usrmgr.NewService(
ctx, ctx,
pgClient, pgClient,
@@ -205,6 +215,7 @@ func (impl *Implm) Run(
impl.cfg.AWS.Bucket, impl.cfg.AWS.Bucket,
impl.cfg.Hostname, impl.cfg.Hostname,
impl.cfg.Auth.Cookie.Secret, impl.cfg.Auth.Cookie.Secret,
vendorAssessmentConfig,
) )
if err != nil { if err != nil {
return fmt.Errorf("cannot create probo service: %w", err) return fmt.Errorf("cannot create probo service: %w", err)
@@ -216,6 +227,7 @@ func (impl *Implm) Run(
Probo: proboService, Probo: proboService,
Usrmgr: usrmgrService, Usrmgr: usrmgrService,
ConnectorRegistry: defaultConnectorRegistry, ConnectorRegistry: defaultConnectorRegistry,
VendorAssessment: vendorAssessment,
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname}, SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname},
Logger: l.Named("http.server"), Logger: l.Named("http.server"),
Auth: console_v1.AuthConfig{ Auth: console_v1.AuthConfig{

View File

@@ -1088,6 +1088,8 @@ type Mutation {
): CreateVendorRiskAssessmentPayload! ): CreateVendorRiskAssessmentPayload!
exportAudit(input: ExportAuditInput!): ExportAuditPayload! exportAudit(input: ExportAuditInput!): ExportAuditPayload!
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
} }
# Input Types # Input Types
@@ -1779,3 +1781,12 @@ input ExportAuditInput {
type ExportAuditPayload { type ExportAuditPayload {
url: String! url: String!
} }
input AssessVendorInput {
id: ID!
websiteUrl: String!
}
type AssessVendorPayload {
vendor: Vendor!
}

View File

@@ -65,6 +65,10 @@ type DirectiveRoot struct {
} }
type ComplexityRoot struct { type ComplexityRoot struct {
AssessVendorPayload struct {
Vendor func(childComplexity int) int
}
AssignTaskPayload struct { AssignTaskPayload struct {
Task func(childComplexity int) int Task func(childComplexity int) int
} }
@@ -329,6 +333,7 @@ type ComplexityRoot struct {
} }
Mutation struct { Mutation struct {
AssessVendor func(childComplexity int, input types.AssessVendorInput) int
AssignTask func(childComplexity int, input types.AssignTaskInput) int AssignTask func(childComplexity int, input types.AssignTaskInput) int
ConfirmEmail func(childComplexity int, input types.ConfirmEmailInput) int ConfirmEmail func(childComplexity int, input types.ConfirmEmailInput) int
CreateControlMeasureMapping func(childComplexity int, input types.CreateControlMeasureMappingInput) int CreateControlMeasureMapping func(childComplexity int, input types.CreateControlMeasureMappingInput) int
@@ -827,6 +832,7 @@ type MutationResolver interface {
SendSigningNotifications(ctx context.Context, input types.SendSigningNotificationsInput) (*types.SendSigningNotificationsPayload, error) SendSigningNotifications(ctx context.Context, input types.SendSigningNotificationsInput) (*types.SendSigningNotificationsPayload, error)
CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error)
ExportAudit(ctx context.Context, input types.ExportAuditInput) (*types.ExportAuditPayload, error) ExportAudit(ctx context.Context, input types.ExportAuditInput) (*types.ExportAuditPayload, error)
AssessVendor(ctx context.Context, input types.AssessVendorInput) (*types.AssessVendorPayload, error)
} }
type OrganizationResolver interface { type OrganizationResolver interface {
LogoURL(ctx context.Context, obj *types.Organization) (*string, error) LogoURL(ctx context.Context, obj *types.Organization) (*string, error)
@@ -919,6 +925,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
_ = ec _ = ec
switch typeName + "." + field { switch typeName + "." + field {
case "AssessVendorPayload.vendor":
if e.complexity.AssessVendorPayload.Vendor == nil {
break
}
return e.complexity.AssessVendorPayload.Vendor(childComplexity), true
case "AssignTaskPayload.task": case "AssignTaskPayload.task":
if e.complexity.AssignTaskPayload.Task == nil { if e.complexity.AssignTaskPayload.Task == nil {
break break
@@ -1724,6 +1737,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.MeasureEdge.Node(childComplexity), true return e.complexity.MeasureEdge.Node(childComplexity), true
case "Mutation.assessVendor":
if e.complexity.Mutation.AssessVendor == nil {
break
}
args, err := ec.field_Mutation_assessVendor_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.AssessVendor(childComplexity, args["input"].(types.AssessVendorInput)), true
case "Mutation.assignTask": case "Mutation.assignTask":
if e.complexity.Mutation.AssignTask == nil { if e.complexity.Mutation.AssignTask == nil {
break break
@@ -3889,6 +3914,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
opCtx := graphql.GetOperationContext(ctx) opCtx := graphql.GetOperationContext(ctx)
ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)}
inputUnmarshalMap := graphql.BuildUnmarshalerMap( inputUnmarshalMap := graphql.BuildUnmarshalerMap(
ec.unmarshalInputAssessVendorInput,
ec.unmarshalInputAssignTaskInput, ec.unmarshalInputAssignTaskInput,
ec.unmarshalInputConfirmEmailInput, ec.unmarshalInputConfirmEmailInput,
ec.unmarshalInputConnectorOrder, ec.unmarshalInputConnectorOrder,
@@ -5147,6 +5173,8 @@ type Mutation {
): CreateVendorRiskAssessmentPayload! ): CreateVendorRiskAssessmentPayload!
exportAudit(input: ExportAuditInput!): ExportAuditPayload! exportAudit(input: ExportAuditInput!): ExportAuditPayload!
assessVendor(input: AssessVendorInput!): AssessVendorPayload!
} }
# Input Types # Input Types
@@ -5838,6 +5866,15 @@ input ExportAuditInput {
type ExportAuditPayload { type ExportAuditPayload {
url: String! url: String!
} }
input AssessVendorInput {
id: ID!
websiteUrl: String!
}
type AssessVendorPayload {
vendor: Vendor!
}
`, BuiltIn: false}, `, BuiltIn: false},
} }
var parsedSchema = gqlparser.MustLoadSchema(sources...) var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -6511,6 +6548,29 @@ func (ec *executionContext) field_Measure_tasks_argsOrderBy(
return zeroVal, nil return zeroVal, nil
} }
func (ec *executionContext) field_Mutation_assessVendor_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_assessVendor_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_assessVendor_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.AssessVendorInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNAssessVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssessVendorInput(ctx, tmp)
}
var zeroVal types.AssessVendorInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_assignTask_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { func (ec *executionContext) field_Mutation_assignTask_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error var err error
args := map[string]any{} args := map[string]any{}
@@ -9699,6 +9759,94 @@ func (ec *executionContext) field___Type_fields_argsIncludeDeprecated(
// region **************************** field.gotpl ***************************** // region **************************** field.gotpl *****************************
func (ec *executionContext) _AssessVendorPayload_vendor(ctx context.Context, field graphql.CollectedField, obj *types.AssessVendorPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_AssessVendorPayload_vendor(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.Vendor, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.Vendor)
fc.Result = res
return ec.marshalNVendor2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVendor(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_AssessVendorPayload_vendor(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "AssessVendorPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "id":
return ec.fieldContext_Vendor_id(ctx, field)
case "name":
return ec.fieldContext_Vendor_name(ctx, field)
case "description":
return ec.fieldContext_Vendor_description(ctx, field)
case "organization":
return ec.fieldContext_Vendor_organization(ctx, field)
case "complianceReports":
return ec.fieldContext_Vendor_complianceReports(ctx, field)
case "riskAssessments":
return ec.fieldContext_Vendor_riskAssessments(ctx, field)
case "businessOwner":
return ec.fieldContext_Vendor_businessOwner(ctx, field)
case "securityOwner":
return ec.fieldContext_Vendor_securityOwner(ctx, field)
case "statusPageUrl":
return ec.fieldContext_Vendor_statusPageUrl(ctx, field)
case "termsOfServiceUrl":
return ec.fieldContext_Vendor_termsOfServiceUrl(ctx, field)
case "privacyPolicyUrl":
return ec.fieldContext_Vendor_privacyPolicyUrl(ctx, field)
case "serviceLevelAgreementUrl":
return ec.fieldContext_Vendor_serviceLevelAgreementUrl(ctx, field)
case "dataProcessingAgreementUrl":
return ec.fieldContext_Vendor_dataProcessingAgreementUrl(ctx, field)
case "certifications":
return ec.fieldContext_Vendor_certifications(ctx, field)
case "securityPageUrl":
return ec.fieldContext_Vendor_securityPageUrl(ctx, field)
case "trustPageUrl":
return ec.fieldContext_Vendor_trustPageUrl(ctx, field)
case "headquarterAddress":
return ec.fieldContext_Vendor_headquarterAddress(ctx, field)
case "legalName":
return ec.fieldContext_Vendor_legalName(ctx, field)
case "websiteUrl":
return ec.fieldContext_Vendor_websiteUrl(ctx, field)
case "createdAt":
return ec.fieldContext_Vendor_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_Vendor_updatedAt(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Vendor", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _AssignTaskPayload_task(ctx context.Context, field graphql.CollectedField, obj *types.AssignTaskPayload) (ret graphql.Marshaler) { func (ec *executionContext) _AssignTaskPayload_task(ctx context.Context, field graphql.CollectedField, obj *types.AssignTaskPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_AssignTaskPayload_task(ctx, field) fc, err := ec.fieldContext_AssignTaskPayload_task(ctx, field)
if err != nil { if err != nil {
@@ -18173,6 +18321,65 @@ func (ec *executionContext) fieldContext_Mutation_exportAudit(ctx context.Contex
return fc, nil return fc, nil
} }
func (ec *executionContext) _Mutation_assessVendor(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_assessVendor(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().AssessVendor(rctx, fc.Args["input"].(types.AssessVendorInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.AssessVendorPayload)
fc.Result = res
return ec.marshalNAssessVendorPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssessVendorPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_assessVendor(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "vendor":
return ec.fieldContext_AssessVendorPayload_vendor(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type AssessVendorPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_assessVendor_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) { func (ec *executionContext) _Organization_id(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Organization_id(ctx, field) fc, err := ec.fieldContext_Organization_id(ctx, field)
if err != nil { if err != nil {
@@ -30810,6 +31017,40 @@ func (ec *executionContext) fieldContext___Type_isOneOf(_ context.Context, field
// region **************************** input.gotpl ***************************** // region **************************** input.gotpl *****************************
func (ec *executionContext) unmarshalInputAssessVendorInput(ctx context.Context, obj any) (types.AssessVendorInput, error) {
var it types.AssessVendorInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"id", "websiteUrl"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "id":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
if err != nil {
return it, err
}
it.ID = data
case "websiteUrl":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("websiteUrl"))
data, err := ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
it.WebsiteURL = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputAssignTaskInput(ctx context.Context, obj any) (types.AssignTaskInput, error) { func (ec *executionContext) unmarshalInputAssignTaskInput(ctx context.Context, obj any) (types.AssignTaskInput, error) {
var it types.AssignTaskInput var it types.AssignTaskInput
asMap := map[string]any{} asMap := map[string]any{}
@@ -33899,6 +34140,45 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj
// region **************************** object.gotpl **************************** // region **************************** object.gotpl ****************************
var assessVendorPayloadImplementors = []string{"AssessVendorPayload"}
func (ec *executionContext) _AssessVendorPayload(ctx context.Context, sel ast.SelectionSet, obj *types.AssessVendorPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, assessVendorPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("AssessVendorPayload")
case "vendor":
out.Values[i] = ec._AssessVendorPayload_vendor(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var assignTaskPayloadImplementors = []string{"AssignTaskPayload"} var assignTaskPayloadImplementors = []string{"AssignTaskPayload"}
func (ec *executionContext) _AssignTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *types.AssignTaskPayload) graphql.Marshaler { func (ec *executionContext) _AssignTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *types.AssignTaskPayload) graphql.Marshaler {
@@ -36929,6 +37209,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
out.Invalids++ out.Invalids++
} }
case "assessVendor":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_assessVendor(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@@ -41377,6 +41664,25 @@ func (ec *executionContext) ___Type(ctx context.Context, sel ast.SelectionSet, o
// region ***************************** type.gotpl ***************************** // region ***************************** type.gotpl *****************************
func (ec *executionContext) unmarshalNAssessVendorInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssessVendorInput(ctx context.Context, v any) (types.AssessVendorInput, error) {
res, err := ec.unmarshalInputAssessVendorInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNAssessVendorPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssessVendorPayload(ctx context.Context, sel ast.SelectionSet, v types.AssessVendorPayload) graphql.Marshaler {
return ec._AssessVendorPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNAssessVendorPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssessVendorPayload(ctx context.Context, sel ast.SelectionSet, v *types.AssessVendorPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._AssessVendorPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNAssignTaskInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssignTaskInput(ctx context.Context, v any) (types.AssignTaskInput, error) { func (ec *executionContext) unmarshalNAssignTaskInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐAssignTaskInput(ctx context.Context, v any) (types.AssignTaskInput, error) {
res, err := ec.unmarshalInputAssignTaskInput(ctx, v) res, err := ec.unmarshalInputAssignTaskInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err) return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -20,6 +20,15 @@ type Node interface {
GetID() gid.GID GetID() gid.GID
} }
type AssessVendorInput struct {
ID gid.GID `json:"id"`
WebsiteURL string `json:"websiteUrl"`
}
type AssessVendorPayload struct {
Vendor *Vendor `json:"vendor"`
}
type AssignTaskInput struct { type AssignTaskInput struct {
TaskID gid.GID `json:"taskId"` TaskID gid.GID `json:"taskId"`
AssignedToID gid.GID `json:"assignedToId"` AssignedToID gid.GID `json:"assignedToId"`

View File

@@ -1316,6 +1316,23 @@ func (r *mutationResolver) ExportAudit(ctx context.Context, input types.ExportAu
}, nil }, nil
} }
// AssessVendor is the resolver for the assessVendor field.
func (r *mutationResolver) AssessVendor(ctx context.Context, input types.AssessVendorInput) (*types.AssessVendorPayload, error) {
svc := GetTenantService(ctx, r.proboSvc, input.ID.TenantID())
vendor, err := svc.Vendors.Assess(ctx, probo.AssessVendorRequest{
ID: input.ID,
WebsiteURL: input.WebsiteURL,
})
if err != nil {
return nil, fmt.Errorf("cannot assess vendor: %w", err)
}
return &types.AssessVendorPayload{
Vendor: types.NewVendor(vendor),
}, nil
}
// LogoURL is the resolver for the logoUrl field. // LogoURL is the resolver for the logoUrl field.
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) { func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID()) svc := GetTenantService(ctx, r.proboSvc, obj.ID.TenantID())

View File

@@ -19,6 +19,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/getprobo/probo/pkg/agents"
"github.com/getprobo/probo/pkg/connector" "github.com/getprobo/probo/pkg/connector"
"github.com/getprobo/probo/pkg/probo" "github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/saferedirect" "github.com/getprobo/probo/pkg/saferedirect"
@@ -37,6 +38,7 @@ type Config struct {
Usrmgr *usrmgr.Service Usrmgr *usrmgr.Service
Auth console_v1.AuthConfig Auth console_v1.AuthConfig
ConnectorRegistry *connector.ConnectorRegistry ConnectorRegistry *connector.ConnectorRegistry
VendorAssessment *agents.VendorAssessment
SafeRedirect *saferedirect.SafeRedirect SafeRedirect *saferedirect.SafeRedirect
Logger *log.Logger Logger *log.Logger
} }