Files
probo/pkg/riskmanagement/service.go
Sacha Al Himdani b9262b5150 Add risk assessment system
Introduce a hierarchical risk assessment model with six entity types:

- Risk Assessment: top-level container scoped to an organization
- Risk Assessment Scope: sub-container for scoping threat modeling
  exercises within an assessment
- Risk Assessment Node: DFD elements typed as ENTITY, BOUNDARY,
  ASSET, or DATA within a scope
- Risk Assessment Process: directed data flows between two nodes
- Risk Assessment Threat: descriptive threats attached to a process
  with a free-text category (e.g. Confidentiality, Integrity)
- Risk Scenario: thin join linking a threat to a risk from the
  register, carrying only a name and description

Risk scoring (likelihood, impact, treatment) remains on the existing
Risk entity. Threats are purely descriptive. Risk Scenarios connect
the threat model to the risk register without duplicating scores.

Backend: migration with PG enum for node types, coredata structs,
service layer with full CRUD and validation, GraphQL schema with
18 mutations and paginated connections, authorization actions and
policies, and base_resolvers.go Node dispatch for all entity types.

Frontend: Risk Assessments list page with create dialog, detail page
showing scopes as cards with nodes/processes/threats tables, inline
create/edit/delete actions on all entities, and a Scenarios tab on
the Risk detail page linking threats to risks. Existing RiskGraph.ts
hook file removed in favor of colocated queries in page files.

E2E tests cover CRUD for all entity types, RBAC, and tenant
isolation.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-19 21:44:27 +02:00

1438 lines
43 KiB
Go

// Copyright (c) 2026 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 riskmanagement
import (
"context"
"fmt"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
const (
TitleMaxLength = 1000
ContentMaxLength = 5000
)
type Service struct {
pg *pg.Client
}
func NewService(pgClient *pg.Client) *Service {
return &Service{pg: pgClient}
}
type (
CreateRiskAssessmentRequest struct {
OrganizationID gid.GID
Name string
Description *string
}
UpdateRiskAssessmentRequest struct {
ID gid.GID
Name *string
Description **string
}
CreateRiskAssessmentScopeRequest struct {
RiskAssessmentID gid.GID
Name string
}
UpdateRiskAssessmentScopeRequest struct {
ID gid.GID
Name *string
}
CreateRiskAssessmentNodeRequest struct {
RiskAssessmentScopeID gid.GID
NodeType coredata.RiskAssessmentNodeType
Name string
}
UpdateRiskAssessmentNodeRequest struct {
ID gid.GID
NodeType *coredata.RiskAssessmentNodeType
Name *string
}
CreateRiskAssessmentProcessRequest struct {
RiskAssessmentScopeID gid.GID
SourceNodeID gid.GID
TargetNodeID gid.GID
Name string
}
UpdateRiskAssessmentProcessRequest struct {
ID gid.GID
SourceNodeID *gid.GID
TargetNodeID *gid.GID
Name *string
}
CreateRiskAssessmentThreatRequest struct {
RiskAssessmentScopeID gid.GID
ProcessID gid.GID
Name string
Category string
}
UpdateRiskAssessmentThreatRequest struct {
ID gid.GID
ProcessID *gid.GID
Name *string
Category *string
}
CreateRiskAssessmentScenarioRequest struct {
RiskAssessmentScopeID gid.GID
Name string
Description *string
}
UpdateRiskAssessmentScenarioRequest struct {
ID gid.GID
Name *string
Description **string
}
LinkRiskAssessmentScenarioThreatRequest struct {
RiskAssessmentScenarioID gid.GID
ThreatID gid.GID
}
UnlinkRiskAssessmentScenarioThreatRequest struct {
RiskAssessmentScenarioID gid.GID
ThreatID gid.GID
}
LinkRiskAssessmentScenarioRiskRequest struct {
RiskAssessmentScenarioID gid.GID
RiskID gid.GID
}
UnlinkRiskAssessmentScenarioRiskRequest struct {
RiskAssessmentScenarioID gid.GID
RiskID gid.GID
}
)
func (r *CreateRiskAssessmentRequest) Validate() error {
v := validator.New()
v.Check(r.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (r *UpdateRiskAssessmentRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (r *CreateRiskAssessmentScopeRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentID, "risk_assessment_id", validator.Required(), validator.GID(coredata.RiskAssessmentEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error()
}
func (r *UpdateRiskAssessmentScopeRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error()
}
func (r *CreateRiskAssessmentNodeRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.NodeType, "node_type", validator.Required(), validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
return v.Error()
}
func (r *UpdateRiskAssessmentNodeRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentNodeEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.NodeType, "node_type", validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
return v.Error()
}
func (r *CreateRiskAssessmentProcessRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
v.Check(r.SourceNodeID, "source_node_id", validator.Required(), validator.GID(coredata.RiskAssessmentNodeEntityType))
v.Check(r.TargetNodeID, "target_node_id", validator.Required(), validator.GID(coredata.RiskAssessmentNodeEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error()
}
func (r *UpdateRiskAssessmentProcessRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentProcessEntityType))
v.Check(r.SourceNodeID, "source_node_id", validator.GID(coredata.RiskAssessmentNodeEntityType))
v.Check(r.TargetNodeID, "target_node_id", validator.GID(coredata.RiskAssessmentNodeEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error()
}
func (r *CreateRiskAssessmentThreatRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
v.Check(r.ProcessID, "process_id", validator.Required(), validator.GID(coredata.RiskAssessmentProcessEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.Category, "category", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error()
}
func (r *UpdateRiskAssessmentThreatRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentThreatEntityType))
v.Check(r.ProcessID, "process_id", validator.GID(coredata.RiskAssessmentProcessEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.Category, "category", validator.SafeTextNoNewLine(TitleMaxLength))
return v.Error()
}
func (r *CreateRiskAssessmentScenarioRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (r *LinkRiskAssessmentScenarioThreatRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScenarioID, "risk_scenario_id", validator.Required(), validator.GID(coredata.RiskAssessmentScenarioEntityType))
v.Check(r.ThreatID, "threat_id", validator.Required(), validator.GID(coredata.RiskAssessmentThreatEntityType))
return v.Error()
}
func (r *UnlinkRiskAssessmentScenarioThreatRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScenarioID, "risk_scenario_id", validator.Required(), validator.GID(coredata.RiskAssessmentScenarioEntityType))
v.Check(r.ThreatID, "threat_id", validator.Required(), validator.GID(coredata.RiskAssessmentThreatEntityType))
return v.Error()
}
func (r *LinkRiskAssessmentScenarioRiskRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScenarioID, "risk_scenario_id", validator.Required(), validator.GID(coredata.RiskAssessmentScenarioEntityType))
v.Check(r.RiskID, "risk_id", validator.Required(), validator.GID(coredata.RiskEntityType))
return v.Error()
}
func (r *UnlinkRiskAssessmentScenarioRiskRequest) Validate() error {
v := validator.New()
v.Check(r.RiskAssessmentScenarioID, "risk_scenario_id", validator.Required(), validator.GID(coredata.RiskAssessmentScenarioEntityType))
v.Check(r.RiskID, "risk_id", validator.Required(), validator.GID(coredata.RiskEntityType))
return v.Error()
}
func (r *UpdateRiskAssessmentScenarioRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentScenarioEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(r.Description, "description", validator.SafeText(ContentMaxLength))
return v.Error()
}
func (s *Service) Create(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentRequest) (*coredata.RiskAssessment, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
ra := &coredata.RiskAssessment{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentEntityType),
OrganizationID: req.OrganizationID,
Name: req.Name,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := ra.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk assessment: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return ra, nil
}
func (s *Service) Get(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessment, error) {
ra := &coredata.RiskAssessment{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := ra.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk assessment: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return ra, nil
}
func (s *Service) Update(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentRequest) (*coredata.RiskAssessment, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
ra := &coredata.RiskAssessment{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := ra.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk assessment: %w", err)
}
if req.Name != nil {
ra.Name = *req.Name
}
if req.Description != nil {
ra.Description = *req.Description
}
ra.UpdatedAt = time.Now()
if err := ra.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk assessment: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return ra, nil
}
func (s *Service) Delete(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
ra := &coredata.RiskAssessment{}
if err := ra.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk assessment: %w", err)
}
return nil
},
)
}
func (s *Service) ListForOrganizationID(
ctx context.Context,
scope coredata.Scoper,
organizationID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentOrderField],
) (*page.Page[*coredata.RiskAssessment, coredata.RiskAssessmentOrderField], error) {
var results coredata.RiskAssessments
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor); err != nil {
return fmt.Errorf("cannot list risk assessments: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountForOrganizationID(
ctx context.Context,
scope coredata.Scoper,
organizationID gid.GID,
) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ras := &coredata.RiskAssessments{}
count, err = ras.CountByOrganizationID(ctx, conn, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count risk assessments: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) CreateScope(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentScopeRequest) (*coredata.RiskAssessmentScope, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
raScope := &coredata.RiskAssessmentScope{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentScopeEntityType),
RiskAssessmentID: req.RiskAssessmentID,
Name: req.Name,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
ra := coredata.RiskAssessment{}
if err := ra.LoadByID(ctx, tx, scope, req.RiskAssessmentID); err != nil {
return fmt.Errorf("cannot load risk assessment: %w", err)
}
raScope.OrganizationID = ra.OrganizationID
if err := raScope.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk assessment scope: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return raScope, nil
}
func (s *Service) GetScope(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentScope, error) {
raScope := &coredata.RiskAssessmentScope{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := raScope.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk assessment scope: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return raScope, nil
}
func (s *Service) UpdateScope(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentScopeRequest) (*coredata.RiskAssessmentScope, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
raScope := &coredata.RiskAssessmentScope{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := raScope.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk assessment scope: %w", err)
}
if req.Name != nil {
raScope.Name = *req.Name
}
raScope.UpdatedAt = time.Now()
if err := raScope.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk assessment scope: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return raScope, nil
}
func (s *Service) DeleteScope(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
raScope := &coredata.RiskAssessmentScope{}
if err := raScope.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk assessment scope: %w", err)
}
return nil
},
)
}
func (s *Service) ListScopesForRiskAssessmentID(
ctx context.Context,
scope coredata.Scoper,
riskAssessmentID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentScopeOrderField],
) (*page.Page[*coredata.RiskAssessmentScope, coredata.RiskAssessmentScopeOrderField], error) {
var results coredata.RiskAssessmentScopes
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByRiskAssessmentID(ctx, conn, scope, riskAssessmentID, cursor); err != nil {
return fmt.Errorf("cannot list risk assessment scopes: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountScopesForRiskAssessmentID(ctx context.Context, scope coredata.Scoper, riskAssessmentID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ss := &coredata.RiskAssessmentScopes{}
count, err = ss.CountByRiskAssessmentID(ctx, conn, scope, riskAssessmentID)
if err != nil {
return fmt.Errorf("cannot count risk assessment scopes: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) CreateNode(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentNodeRequest) (*coredata.RiskAssessmentNode, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
node := &coredata.RiskAssessmentNode{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentNodeEntityType),
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
NodeType: req.NodeType,
Name: req.Name,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
raScope := coredata.RiskAssessmentScope{}
if err := raScope.LoadByID(ctx, tx, scope, req.RiskAssessmentScopeID); err != nil {
return fmt.Errorf("cannot load risk assessment scope: %w", err)
}
node.OrganizationID = raScope.OrganizationID
if err := node.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk assessment node: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return node, nil
}
func (s *Service) GetNode(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentNode, error) {
node := &coredata.RiskAssessmentNode{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := node.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk assessment node: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return node, nil
}
func (s *Service) UpdateNode(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentNodeRequest) (*coredata.RiskAssessmentNode, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
node := &coredata.RiskAssessmentNode{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := node.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk assessment node: %w", err)
}
if req.Name != nil {
node.Name = *req.Name
}
if req.NodeType != nil {
node.NodeType = *req.NodeType
}
node.UpdatedAt = time.Now()
if err := node.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk assessment node: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return node, nil
}
func (s *Service) DeleteNode(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
node := &coredata.RiskAssessmentNode{}
if err := node.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk assessment node: %w", err)
}
return nil
},
)
}
func (s *Service) ListNodesForScopeID(
ctx context.Context,
scope coredata.Scoper,
scopeID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentNodeOrderField],
) (*page.Page[*coredata.RiskAssessmentNode, coredata.RiskAssessmentNodeOrderField], error) {
var results coredata.RiskAssessmentNodes
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByRiskAssessmentScopeID(ctx, conn, scope, scopeID, cursor); err != nil {
return fmt.Errorf("cannot list risk assessment nodes: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountNodesForScopeID(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ns := &coredata.RiskAssessmentNodes{}
count, err = ns.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
if err != nil {
return fmt.Errorf("cannot count risk assessment nodes: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) CreateProcess(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentProcessRequest) (*coredata.RiskAssessmentProcess, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
process := &coredata.RiskAssessmentProcess{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentProcessEntityType),
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
SourceNodeID: req.SourceNodeID,
TargetNodeID: req.TargetNodeID,
Name: req.Name,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
raScope := coredata.RiskAssessmentScope{}
if err := raScope.LoadByID(ctx, tx, scope, req.RiskAssessmentScopeID); err != nil {
return fmt.Errorf("cannot load risk assessment scope: %w", err)
}
process.OrganizationID = raScope.OrganizationID
if err := s.assertNodeInScope(ctx, tx, scope, req.SourceNodeID, req.RiskAssessmentScopeID, "source_node_id"); err != nil {
return err
}
if err := s.assertNodeInScope(ctx, tx, scope, req.TargetNodeID, req.RiskAssessmentScopeID, "target_node_id"); err != nil {
return err
}
if err := process.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk assessment process: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return process, nil
}
func (s *Service) GetProcess(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentProcess, error) {
process := &coredata.RiskAssessmentProcess{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := process.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk assessment process: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return process, nil
}
func (s *Service) UpdateProcess(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentProcessRequest) (*coredata.RiskAssessmentProcess, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
process := &coredata.RiskAssessmentProcess{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := process.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk assessment process: %w", err)
}
if req.SourceNodeID != nil {
if err := s.assertNodeInScope(ctx, tx, scope, *req.SourceNodeID, process.RiskAssessmentScopeID, "source_node_id"); err != nil {
return err
}
process.SourceNodeID = *req.SourceNodeID
}
if req.TargetNodeID != nil {
if err := s.assertNodeInScope(ctx, tx, scope, *req.TargetNodeID, process.RiskAssessmentScopeID, "target_node_id"); err != nil {
return err
}
process.TargetNodeID = *req.TargetNodeID
}
if req.Name != nil {
process.Name = *req.Name
}
process.UpdatedAt = time.Now()
if err := process.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk assessment process: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return process, nil
}
func (s *Service) DeleteProcess(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
process := &coredata.RiskAssessmentProcess{}
if err := process.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk assessment process: %w", err)
}
return nil
},
)
}
func (s *Service) ListProcessesForScopeID(
ctx context.Context,
scope coredata.Scoper,
scopeID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentProcessOrderField],
) (*page.Page[*coredata.RiskAssessmentProcess, coredata.RiskAssessmentProcessOrderField], error) {
var results coredata.RiskAssessmentProcesses
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByRiskAssessmentScopeID(ctx, conn, scope, scopeID, cursor); err != nil {
return fmt.Errorf("cannot list risk assessment processes: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountProcessesForScopeID(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ps := &coredata.RiskAssessmentProcesses{}
count, err = ps.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
if err != nil {
return fmt.Errorf("cannot count risk assessment processes: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) CreateThreat(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentThreatRequest) (*coredata.RiskAssessmentThreat, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
threat := &coredata.RiskAssessmentThreat{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentThreatEntityType),
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
ProcessID: req.ProcessID,
Name: req.Name,
Category: req.Category,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
raScope := coredata.RiskAssessmentScope{}
if err := raScope.LoadByID(ctx, tx, scope, req.RiskAssessmentScopeID); err != nil {
return fmt.Errorf("cannot load risk assessment scope: %w", err)
}
threat.OrganizationID = raScope.OrganizationID
if err := s.assertProcessInScope(ctx, tx, scope, req.ProcessID, req.RiskAssessmentScopeID, "process_id"); err != nil {
return err
}
if err := threat.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk threat: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return threat, nil
}
func (s *Service) GetThreat(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentThreat, error) {
threat := &coredata.RiskAssessmentThreat{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := threat.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk threat: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return threat, nil
}
func (s *Service) UpdateThreat(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentThreatRequest) (*coredata.RiskAssessmentThreat, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
threat := &coredata.RiskAssessmentThreat{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := threat.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk threat: %w", err)
}
if req.ProcessID != nil {
if err := s.assertProcessInScope(ctx, tx, scope, *req.ProcessID, threat.RiskAssessmentScopeID, "process_id"); err != nil {
return err
}
threat.ProcessID = *req.ProcessID
}
if req.Name != nil {
threat.Name = *req.Name
}
if req.Category != nil {
threat.Category = *req.Category
}
threat.UpdatedAt = time.Now()
if err := threat.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk threat: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return threat, nil
}
func (s *Service) DeleteThreat(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
threat := &coredata.RiskAssessmentThreat{}
if err := threat.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk threat: %w", err)
}
return nil
},
)
}
func (s *Service) ListThreatsForScopeID(
ctx context.Context,
scope coredata.Scoper,
scopeID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentThreatOrderField],
) (*page.Page[*coredata.RiskAssessmentThreat, coredata.RiskAssessmentThreatOrderField], error) {
var results coredata.RiskAssessmentThreats
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByRiskAssessmentScopeID(ctx, conn, scope, scopeID, cursor); err != nil {
return fmt.Errorf("cannot list risk threats: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountThreatsForScopeID(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ts := &coredata.RiskAssessmentThreats{}
count, err = ts.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
if err != nil {
return fmt.Errorf("cannot count risk threats: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) CreateScenario(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentScenarioRequest) (*coredata.RiskAssessmentScenario, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
scenario := &coredata.RiskAssessmentScenario{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentScenarioEntityType),
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
Name: req.Name,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
raScope := coredata.RiskAssessmentScope{}
if err := raScope.LoadByID(ctx, tx, scope, req.RiskAssessmentScopeID); err != nil {
return fmt.Errorf("cannot load risk assessment scope: %w", err)
}
scenario.OrganizationID = raScope.OrganizationID
if err := scenario.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk scenario: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return scenario, nil
}
func (s *Service) GetScenario(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentScenario, error) {
scenario := &coredata.RiskAssessmentScenario{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := scenario.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk scenario: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return scenario, nil
}
func (s *Service) UpdateScenario(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentScenarioRequest) (*coredata.RiskAssessmentScenario, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
scenario := &coredata.RiskAssessmentScenario{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := scenario.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk scenario: %w", err)
}
if req.Name != nil {
scenario.Name = *req.Name
}
if req.Description != nil {
scenario.Description = *req.Description
}
scenario.UpdatedAt = time.Now()
if err := scenario.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk scenario: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return scenario, nil
}
func (s *Service) DeleteScenario(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
scenario := &coredata.RiskAssessmentScenario{}
if err := scenario.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk scenario: %w", err)
}
return nil
},
)
}
func (s *Service) ListScenariosForOrganizationID(
ctx context.Context,
scope coredata.Scoper,
organizationID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentScenarioOrderField],
) (*page.Page[*coredata.RiskAssessmentScenario, coredata.RiskAssessmentScenarioOrderField], error) {
var results coredata.RiskAssessmentScenarios
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByOrganizationID(ctx, conn, scope, organizationID, cursor); err != nil {
return fmt.Errorf("cannot list risk scenarios: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountScenariosForOrganizationID(
ctx context.Context,
scope coredata.Scoper,
organizationID gid.GID,
) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ss := &coredata.RiskAssessmentScenarios{}
count, err = ss.CountByOrganizationID(ctx, conn, scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count risk scenarios: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) ListScenariosForRiskID(
ctx context.Context,
scope coredata.Scoper,
riskID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentScenarioOrderField],
) (*page.Page[*coredata.RiskAssessmentScenario, coredata.RiskAssessmentScenarioOrderField], error) {
var results coredata.RiskAssessmentScenarios
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByRiskID(ctx, conn, scope, riskID, cursor); err != nil {
return fmt.Errorf("cannot list risk scenarios: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountScenariosForRiskID(ctx context.Context, scope coredata.Scoper, riskID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ss := &coredata.RiskAssessmentScenarios{}
count, err = ss.CountByRiskID(ctx, conn, scope, riskID)
if err != nil {
return fmt.Errorf("cannot count risk scenarios: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) ListScenariosForScopeID(
ctx context.Context,
scope coredata.Scoper,
scopeID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentScenarioOrderField],
) (*page.Page[*coredata.RiskAssessmentScenario, coredata.RiskAssessmentScenarioOrderField], error) {
var results coredata.RiskAssessmentScenarios
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByRiskAssessmentScopeID(ctx, conn, scope, scopeID, cursor); err != nil {
return fmt.Errorf("cannot list risk scenarios: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountScenariosForScopeID(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ss := &coredata.RiskAssessmentScenarios{}
count, err = ss.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
if err != nil {
return fmt.Errorf("cannot count risk scenarios: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) LinkScenarioThreat(ctx context.Context, scope coredata.Scoper, req LinkRiskAssessmentScenarioThreatRequest) error {
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid request: %w", err)
}
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
scenario := coredata.RiskAssessmentScenario{}
if err := scenario.LoadByID(ctx, tx, scope, req.RiskAssessmentScenarioID); err != nil {
return fmt.Errorf("cannot load risk scenario: %w", err)
}
threat := coredata.RiskAssessmentThreat{}
if err := threat.LoadByID(ctx, tx, scope, req.ThreatID); err != nil {
return fmt.Errorf("cannot load threat: %w", err)
}
if scenario.OrganizationID != threat.OrganizationID {
return validator.ValidationErrors{{
Field: "threat_id",
Code: validator.ErrorCodeCustom,
Message: "threat and scenario must belong to the same organization",
}}
}
link := &coredata.RiskAssessmentScenarioThreat{
RiskAssessmentScenarioID: req.RiskAssessmentScenarioID,
RiskAssessmentThreatID: req.ThreatID,
CreatedAt: time.Now(),
}
if err := link.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot link scenario threat: %w", err)
}
return nil
},
)
}
func (s *Service) UnlinkScenarioThreat(ctx context.Context, scope coredata.Scoper, req UnlinkRiskAssessmentScenarioThreatRequest) error {
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid request: %w", err)
}
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
link := &coredata.RiskAssessmentScenarioThreat{
RiskAssessmentScenarioID: req.RiskAssessmentScenarioID,
RiskAssessmentThreatID: req.ThreatID,
}
if err := link.Delete(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot unlink scenario threat: %w", err)
}
return nil
},
)
}
func (s *Service) LinkScenarioRisk(ctx context.Context, scope coredata.Scoper, req LinkRiskAssessmentScenarioRiskRequest) error {
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid request: %w", err)
}
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
scenario := coredata.RiskAssessmentScenario{}
if err := scenario.LoadByID(ctx, tx, scope, req.RiskAssessmentScenarioID); err != nil {
return fmt.Errorf("cannot load risk scenario: %w", err)
}
risk := coredata.Risk{}
if err := risk.LoadByID(ctx, tx, scope, req.RiskID); err != nil {
return fmt.Errorf("cannot load risk: %w", err)
}
if scenario.OrganizationID != risk.OrganizationID {
return validator.ValidationErrors{{
Field: "risk_id",
Code: validator.ErrorCodeCustom,
Message: "risk and scenario must belong to the same organization",
}}
}
link := &coredata.RiskAssessmentScenarioRisk{
RiskAssessmentScenarioID: req.RiskAssessmentScenarioID,
RiskID: req.RiskID,
CreatedAt: time.Now(),
}
if err := link.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot link scenario risk: %w", err)
}
return nil
},
)
}
func (s *Service) UnlinkScenarioRisk(ctx context.Context, scope coredata.Scoper, req UnlinkRiskAssessmentScenarioRiskRequest) error {
if err := req.Validate(); err != nil {
return fmt.Errorf("invalid request: %w", err)
}
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
link := &coredata.RiskAssessmentScenarioRisk{
RiskAssessmentScenarioID: req.RiskAssessmentScenarioID,
RiskID: req.RiskID,
}
if err := link.Delete(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot unlink scenario risk: %w", err)
}
return nil
},
)
}
func (s *Service) ListThreatsForScenarioID(
ctx context.Context,
scope coredata.Scoper,
scenarioID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentThreatOrderField],
) (*page.Page[*coredata.RiskAssessmentThreat, coredata.RiskAssessmentThreatOrderField], error) {
var results coredata.RiskAssessmentThreats
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByScenarioID(ctx, conn, scope, scenarioID, cursor); err != nil {
return fmt.Errorf("cannot list scenario threats: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountThreatsForScenarioID(ctx context.Context, scope coredata.Scoper, scenarioID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
ts := &coredata.RiskAssessmentThreats{}
count, err = ts.CountByScenarioID(ctx, conn, scope, scenarioID)
if err != nil {
return fmt.Errorf("cannot count scenario threats: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) ListRisksForScenarioID(
ctx context.Context,
scope coredata.Scoper,
scenarioID gid.GID,
cursor *page.Cursor[coredata.RiskOrderField],
) (*page.Page[*coredata.Risk, coredata.RiskOrderField], error) {
var results coredata.Risks
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := results.LoadByScenarioID(ctx, conn, scope, scenarioID, cursor); err != nil {
return fmt.Errorf("cannot list scenario risks: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountRisksForScenarioID(ctx context.Context, scope coredata.Scoper, scenarioID gid.GID) (int, error) {
var count int
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
rs := &coredata.Risks{}
count, err = rs.CountByScenarioID(ctx, conn, scope, scenarioID)
if err != nil {
return fmt.Errorf("cannot count scenario risks: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s *Service) assertNodeInScope(
ctx context.Context,
tx pg.Tx,
scope coredata.Scoper,
nodeID gid.GID,
scopeID gid.GID,
field string,
) error {
node := &coredata.RiskAssessmentNode{}
if err := node.LoadByID(ctx, tx, scope, nodeID); err != nil {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "node not found",
}}
}
if node.RiskAssessmentScopeID != scopeID {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "node does not belong to this scope",
}}
}
return nil
}
func (s *Service) assertProcessInScope(
ctx context.Context,
tx pg.Tx,
scope coredata.Scoper,
processID gid.GID,
scopeID gid.GID,
field string,
) error {
process := &coredata.RiskAssessmentProcess{}
if err := process.LoadByID(ctx, tx, scope, processID); err != nil {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "process not found",
}}
}
if process.RiskAssessmentScopeID != scopeID {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "process does not belong to this scope",
}}
}
return nil
}