Add async third-party vetting

Queue vetting on third_parties with PENDING, PROCESSING,
COMPLETED, and FAILED states. Expose enqueue and status through
GraphQL, MCP, CLI, and n8n, validate vet requests, tune the
worker via config, and poll the detail page while vetting runs.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-05-27 13:34:49 +02:00
parent 1a71d15bc5
commit 6e7c96732f
59 changed files with 3251 additions and 520 deletions

View File

@@ -32,10 +32,12 @@ import (
"go.probo.inc/probo/pkg/prosemirror"
"go.probo.inc/probo/pkg/riskmanagement"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/thirdparty"
)
type Resolver struct {
proboSvc *probo.Service
thirdPartySvc *thirdparty.Service
iamSvc *iam.Service
accessReview *accessreview.Service
cookieBanner *cookiebanner.Service

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/cookiebanner"
"go.probo.inc/probo/pkg/coredata"
@@ -23,6 +24,8 @@ import (
"go.probo.inc/probo/pkg/riskmanagement"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/mcp/v1/types"
"go.probo.inc/probo/pkg/thirdparty"
"go.probo.inc/probo/pkg/validator"
)
// ListOrganizationsTool handles the listOrganizations tool
@@ -5296,27 +5299,47 @@ func (r *Resolver) DeleteCustomDomainTool(ctx context.Context, req *mcp.CallTool
return nil, types.DeleteCustomDomainOutput{DeletedCustomDomain: deletedDomain}, nil
}
func (r *Resolver) AssessThirdPartyTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AssessThirdPartyInput) (*mcp.CallToolResult, types.AssessThirdPartyOutput, error) {
scope, err := r.Authorize(ctx, input.ID, probo.ActionThirdPartyAssess)
func (r *Resolver) VetThirdPartyTool(ctx context.Context, req *mcp.CallToolRequest, input *types.VetThirdPartyInput) (*mcp.CallToolResult, types.VetThirdPartyOutput, error) {
scope, err := r.Authorize(ctx, input.ID, probo.ActionThirdPartyVet)
if err != nil {
return nil, types.AssessThirdPartyOutput{}, err
return nil, types.VetThirdPartyOutput{}, err
}
svc := r.proboSvc
svc := r.thirdPartySvc
result, err := svc.ThirdParties.Assess(
thirdParty, err := svc.Vet(
ctx, scope,
probo.AssessThirdPartyRequest{
thirdparty.VetRequest{
ID: input.ID,
WebsiteURL: input.WebsiteURL,
Procedure: input.Procedure,
},
)
if err != nil {
return nil, types.AssessThirdPartyOutput{}, fmt.Errorf("cannot assess thirdParty: %w", err)
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, types.VetThirdPartyOutput{}, validationErrors
}
if errors.Is(err, coredata.ErrResourceNotFound) {
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("resource not found")
}
if errors.Is(err, thirdparty.ErrVettingDisabled) {
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("vetting is not configured")
}
if errors.Is(err, thirdparty.ErrVettingInProgress) {
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("vetting is already in progress")
}
r.logger.ErrorCtx(ctx, "cannot vet thirdParty", log.Error(err))
return nil, types.VetThirdPartyOutput{}, fmt.Errorf("internal server error")
}
return nil, types.NewAssessThirdPartyOutput(result), nil
return nil, types.VetThirdPartyOutput{
ThirdParty: types.NewThirdParty(thirdParty),
}, nil
}
func (r *Resolver) PublishFindingListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishFindingListInput) (*mcp.CallToolResult, types.PublishFindingListOutput, error) {

View File

@@ -1480,7 +1480,7 @@ components:
$ref: "#/components/schemas/GID"
description: Deleted thirdParty service ID
AssessThirdPartyInput:
VetThirdPartyInput:
type: object
required:
- id
@@ -1488,48 +1488,21 @@ components:
properties:
id:
$ref: "#/components/schemas/GID"
description: ThirdParty ID to assess
description: ThirdParty ID to vet
website_url:
type: string
description: ThirdParty website URL to crawl and assess
description: ThirdParty website URL to crawl and vet
procedure:
type: string
description: Optional custom assessment procedure (overrides the default)
description: Optional custom vetting procedure (overrides the default)
ThirdPartySubprocessor:
type: object
required:
- name
- country
- purpose
properties:
name:
type: string
description: Sub-processor name
country:
type: string
description: Country where the sub-processor operates
purpose:
type: string
description: Purpose of the sub-processor
AssessThirdPartyOutput:
VetThirdPartyOutput:
type: object
required:
- thirdParty
- report
- subprocessors
properties:
thirdParty:
$ref: "#/components/schemas/ThirdParty"
report:
type: string
description: Markdown-formatted thirdParty assessment report
subprocessors:
type: array
items:
$ref: "#/components/schemas/ThirdPartySubprocessor"
description: Sub-processors discovered during the assessment
GetUserInput:
type: object
@@ -12096,14 +12069,14 @@ tools:
$ref: "#/components/schemas/DeleteThirdPartyServiceInput"
outputSchema:
$ref: "#/components/schemas/DeleteThirdPartyServiceOutput"
- name: assessThirdParty
description: Run an AI-powered assessment on a thirdParty by crawling its website. Returns a markdown report, the discovered sub-processors, and an enriched thirdParty record. Long-running (up to 20 minutes).
- name: vetThirdParty
description: Start AI-powered vetting of a third party by crawling its website. Returns immediately; vetting runs in the background.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/AssessThirdPartyInput"
$ref: "#/components/schemas/VetThirdPartyInput"
outputSchema:
$ref: "#/components/schemas/AssessThirdPartyOutput"
$ref: "#/components/schemas/VetThirdPartyOutput"
- name: listRisks
description: List all risks for the organization
hints:

View File

@@ -17,7 +17,6 @@ package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
)
func NewThirdPartyRiskAssessment(v *coredata.ThirdPartyRiskAssessment) *ThirdPartyRiskAssessment {
@@ -229,24 +228,3 @@ func NewListThirdPartyServicesOutput(p *page.Page[*coredata.ThirdPartyService, c
ThirdPartyServices: services,
}
}
func NewThirdPartySubprocessors(sps []probo.Subprocessor) []*ThirdPartySubprocessor {
result := make([]*ThirdPartySubprocessor, len(sps))
for i, sp := range sps {
result[i] = &ThirdPartySubprocessor{
Name: sp.Name,
Country: sp.Country,
Purpose: sp.Purpose,
}
}
return result
}
func NewAssessThirdPartyOutput(result *probo.AssessThirdPartyResult) AssessThirdPartyOutput {
return AssessThirdPartyOutput{
ThirdParty: NewThirdParty(result.ThirdParty),
Report: result.Report,
Subprocessors: NewThirdPartySubprocessors(result.Subprocessors),
}
}

View File

@@ -29,15 +29,26 @@ import (
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/mcp/mcputils"
"go.probo.inc/probo/pkg/server/api/mcp/v1/server"
"go.probo.inc/probo/pkg/thirdparty"
)
func NewMux(logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service, accessReviewSvc *accessreview.Service, cookieBannerSvc *cookiebanner.Service, riskManagementSvc *riskmanagement.Service, tokenSecret string) *chi.Mux {
func NewMux(
logger *log.Logger,
proboSvc *probo.Service,
thirdPartySvc *thirdparty.Service,
iamSvc *iam.Service,
accessReviewSvc *accessreview.Service,
cookieBannerSvc *cookiebanner.Service,
riskManagementSvc *riskmanagement.Service,
tokenSecret string,
) *chi.Mux {
logger = logger.Named("mcp.v1")
logger.Info("initializing MCP server")
resolver := &Resolver{
proboSvc: proboSvc,
thirdPartySvc: thirdPartySvc,
iamSvc: iamSvc,
accessReview: accessReviewSvc,
cookieBanner: cookieBannerSvc,