Files
probo/pkg/riskmanagement/service.go
Sacha Al Himdani 4c57d201a4 Make license declarations consistently MIT
The source headers, LICENSE files, and license metadata had drifted
apart. Align the entire project to MIT:

- Convert every source-file header to the MIT text across all comment
  styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including
  SPDX-License-Identifier tags
- Set the root and cookie-banner LICENSE files to the MIT text with a
  "MIT License" title line
- Switch the package.json license fields, Docker image label, and
  cookie-banner README to MIT
- Update docs and the genmodels header generator accordingly
- Normalize copyright lines to a single format
  (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the
  hello@getprobo.com and hello@probo.inc emails to hello@probo.com and
  the comma-separated years to a hyphenated range

Genuine third-party references are intentionally left untouched: the
Lucide icon attributions (Lucide is ISC) and the trivy dependency
license allowlist.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
2026-07-13 16:21:14 +02:00

1939 lines
52 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// 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
}
CreateRiskAssessmentBoundaryRequest struct {
RiskAssessmentScopeID gid.GID
ParentBoundaryID *gid.GID
Name string
}
UpdateRiskAssessmentBoundaryRequest struct {
ID gid.GID
ParentBoundaryID **gid.GID
Name *string
}
CreateRiskAssessmentNodeRequest struct {
RiskAssessmentScopeID gid.GID
BoundaryID *gid.GID
NodeType coredata.RiskAssessmentNodeType
Name string
}
UpdateRiskAssessmentNodeRequest struct {
ID gid.GID
BoundaryID **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 *CreateRiskAssessmentBoundaryRequest) 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))
if r.ParentBoundaryID != nil {
v.Check(*r.ParentBoundaryID, "parent_boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
}
return v.Error()
}
func (r *UpdateRiskAssessmentBoundaryRequest) Validate() error {
v := validator.New()
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
if r.ParentBoundaryID != nil && *r.ParentBoundaryID != nil {
v.Check(**r.ParentBoundaryID, "parent_boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
}
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()))
if r.BoundaryID != nil {
v.Check(*r.BoundaryID, "boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
}
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()))
if r.BoundaryID != nil && *r.BoundaryID != nil {
v.Check(**r.BoundaryID, "boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
}
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,
BoundaryID: req.BoundaryID,
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)
}
if req.BoundaryID != nil {
if err := s.assertBoundaryInScope(ctx, tx, scope, *req.BoundaryID, req.RiskAssessmentScopeID, "boundary_id"); err != nil {
return 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
}
if req.BoundaryID != nil {
if *req.BoundaryID != nil {
if err := s.assertBoundaryInScope(ctx, tx, scope, **req.BoundaryID, node.RiskAssessmentScopeID, "boundary_id"); err != nil {
return err
}
}
node.BoundaryID = *req.BoundaryID
}
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) CreateBoundary(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentBoundaryRequest) (*coredata.RiskAssessmentBoundary, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
boundary := &coredata.RiskAssessmentBoundary{
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentBoundaryEntityType),
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
ParentBoundaryID: req.ParentBoundaryID,
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)
}
if req.ParentBoundaryID != nil {
if err := s.assertBoundaryInScope(ctx, tx, scope, *req.ParentBoundaryID, req.RiskAssessmentScopeID, "parent_boundary_id"); err != nil {
return err
}
}
boundary.OrganizationID = raScope.OrganizationID
if err := boundary.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert risk assessment boundary: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return boundary, nil
}
func (s *Service) GetBoundary(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentBoundary, error) {
boundary := &coredata.RiskAssessmentBoundary{}
err := s.pg.WithConn(
ctx,
func(ctx context.Context, conn pg.Querier) error {
if err := boundary.LoadByID(ctx, conn, scope, id); err != nil {
return fmt.Errorf("cannot load risk assessment boundary: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return boundary, nil
}
func (s *Service) UpdateBoundary(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentBoundaryRequest) (*coredata.RiskAssessmentBoundary, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
boundary := &coredata.RiskAssessmentBoundary{}
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := boundary.LoadByID(ctx, tx, scope, req.ID); err != nil {
return fmt.Errorf("cannot load risk assessment boundary: %w", err)
}
if req.Name != nil {
boundary.Name = *req.Name
}
if req.ParentBoundaryID != nil {
if *req.ParentBoundaryID != nil {
if err := s.assertBoundaryInScope(ctx, tx, scope, **req.ParentBoundaryID, boundary.RiskAssessmentScopeID, "parent_boundary_id"); err != nil {
return err
}
if err := s.assertNoBoundaryCycle(ctx, tx, scope, boundary.ID, **req.ParentBoundaryID, "parent_boundary_id"); err != nil {
return err
}
}
boundary.ParentBoundaryID = *req.ParentBoundaryID
}
boundary.UpdatedAt = time.Now()
if err := boundary.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update risk assessment boundary: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return boundary, nil
}
func (s *Service) DeleteBoundary(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
boundary := &coredata.RiskAssessmentBoundary{}
if err := boundary.Delete(ctx, tx, scope, id); err != nil {
return fmt.Errorf("cannot delete risk assessment boundary: %w", err)
}
return nil
},
)
}
func (s *Service) ListBoundariesForScopeID(
ctx context.Context,
scope coredata.Scoper,
scopeID gid.GID,
cursor *page.Cursor[coredata.RiskAssessmentBoundaryOrderField],
) (*page.Page[*coredata.RiskAssessmentBoundary, coredata.RiskAssessmentBoundaryOrderField], error) {
var results coredata.RiskAssessmentBoundaries
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 boundaries: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(results, cursor), nil
}
func (s *Service) CountBoundariesForScopeID(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) {
bs := &coredata.RiskAssessmentBoundaries{}
count, err = bs.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
if err != nil {
return fmt.Errorf("cannot count risk assessment boundaries: %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) assertBoundaryInScope(
ctx context.Context,
tx pg.Tx,
scope coredata.Scoper,
boundaryID gid.GID,
scopeID gid.GID,
field string,
) error {
boundary := &coredata.RiskAssessmentBoundary{}
if err := boundary.LoadByID(ctx, tx, scope, boundaryID); err != nil {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "boundary not found",
}}
}
// A boundary in a different scope is reported identically to a missing
// one so the error does not reveal that the resource exists elsewhere.
if boundary.RiskAssessmentScopeID != scopeID {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "boundary not found",
}}
}
return nil
}
// assertNoBoundaryCycle walks the ancestor chain starting from the proposed
// parent. If it reaches the boundary being updated, the new parent would make
// the boundary an ancestor of itself (a cycle), which is rejected. A visited
// set guards against any pre-existing cycle in stored data.
func (s *Service) assertNoBoundaryCycle(
ctx context.Context,
tx pg.Tx,
scope coredata.Scoper,
boundaryID gid.GID,
proposedParentID gid.GID,
field string,
) error {
visited := make(map[gid.GID]bool)
currentID := proposedParentID
for {
if currentID == boundaryID {
return validator.ValidationErrors{{
Field: field,
Code: validator.ErrorCodeCustom,
Message: "boundary cannot be nested under itself or one of its descendants",
}}
}
if visited[currentID] {
return nil
}
visited[currentID] = true
current := &coredata.RiskAssessmentBoundary{}
if err := current.LoadByID(ctx, tx, scope, currentID); err != nil {
return fmt.Errorf("cannot load parent boundary: %w", err)
}
if current.ParentBoundaryID == nil {
return nil
}
currentID = *current.ParentBoundaryID
}
}
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
}