Add mapping between control and mitigation

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-31 22:01:09 +02:00
parent 38a0458d78
commit d0fc5cd439
20 changed files with 4775 additions and 156 deletions

View File

@@ -30,8 +30,8 @@ type (
Control struct {
ID gid.GID `db:"id"`
ReferenceID string `db:"reference_id"`
FrameworkID gid.GID `db:"framework_id"`
TenantID gid.TenantID `db:"tenant_id"`
FrameworkID gid.GID `db:"framework_id"`
Name string `db:"name"`
Description string `db:"description"`
CreatedAt time.Time `db:"created_at"`
@@ -56,6 +56,65 @@ func (c Control) CursorKey(orderBy ControlOrderField) page.CursorKey {
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (c *Controls) LoadByMitigationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
mitigationID gid.GID,
cursor *page.Cursor[ControlOrderField],
) error {
q := `
WITH ctrl AS (
SELECT
c.id,
c.reference_id,
c.framework_id,
c.tenant_id,
c.name,
c.description,
c.created_at,
c.updated_at
FROM
controls c
INNER JOIN
controls_mitigations cm ON c.id = cm.control_id
WHERE
cm.mitigation_id = @mitigation_id
)
SELECT
id,
reference_id,
framework_id,
tenant_id,
name,
description,
created_at,
updated_at
FROM
ctrl
WHERE %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"mitigation_id": mitigationID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query controls: %w", err)
}
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control])
if err != nil {
return fmt.Errorf("cannot collect controls: %w", err)
}
*c = controls
return nil
}
func (c *Controls) LoadByFrameworkID(
ctx context.Context,
conn pg.Conn,

View File

@@ -43,7 +43,7 @@ func (cm ControlMitigation) Insert(
) error {
q := `
INSERT INTO
control_mitigations (
controls_mitigations (
control_id,
mitigation_id,
tenant_id,
@@ -75,7 +75,7 @@ func (cm ControlMitigation) Delete(
q := `
DELETE
FROM
control_mitigations
controls_mitigations
WHERE
%s
AND control_id = @control_id

View File

@@ -30,6 +30,7 @@ import (
type (
Mitigation struct {
ID gid.GID `db:"id"`
TenantID gid.TenantID `db:"tenant_id"`
OrganizationID gid.GID `db:"organization_id"`
Category string `db:"category"`
Name string `db:"name"`
@@ -39,7 +40,6 @@ type (
ContentRef string `db:"content_ref"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Version int `db:"version"`
Standards []string `db:"standards"`
}
@@ -55,6 +55,123 @@ func (c Mitigation) CursorKey(orderBy MitigationOrderField) page.CursorKey {
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (c *Mitigations) LoadByControlID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
controlID gid.GID,
cursor *page.Cursor[MitigationOrderField],
) error {
q := `
WITH mtgtns AS (
SELECT
m.id,
m.tenant_id,
m.organization_id,
m.category,
m.name,
m.description,
m.state,
m.importance,
m.content_ref,
m.created_at,
m.updated_at,
m.standards
FROM
mitigations m
INNER JOIN
controls_mitigations cm ON m.id = cm.mitigation_id
WHERE
cm.control_id = @control_id
)
SELECT
id,
tenant_id,
organization_id,
category,
name,
description,
state,
importance,
content_ref,
created_at,
updated_at,
standards
FROM
mtgtns
WHERE %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"control_id": controlID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query mitigations: %w", err)
}
mitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Mitigation])
if err != nil {
return fmt.Errorf("cannot collect mitigations: %w", err)
}
*c = mitigations
return nil
}
func (c *Mitigations) LoadByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
cursor *page.Cursor[MitigationOrderField],
) error {
q := `
SELECT
id,
tenant_id,
organization_id,
category,
name,
description,
state,
importance,
content_ref,
created_at,
updated_at,
standards
FROM
mitigations
WHERE
%s
AND organization_id = @organization_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query mitigations: %w", err)
}
mitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Mitigation])
if err != nil {
return fmt.Errorf("cannot collect mitigations: %w", err)
}
*c = mitigations
return nil
}
func (c *Mitigation) LoadByID(
ctx context.Context,
conn pg.Conn,
@@ -64,6 +181,7 @@ func (c *Mitigation) LoadByID(
q := `
SELECT
id,
tenant_id,
organization_id,
category,
name,
@@ -73,8 +191,7 @@ SELECT
content_ref,
created_at,
updated_at,
standards,
version
standards
FROM
mitigations
WHERE
@@ -122,8 +239,7 @@ INSERT INTO
content_ref,
created_at,
updated_at,
standards,
version
standards
)
VALUES (
@tenant_id,
@@ -137,8 +253,7 @@ VALUES (
@content_ref,
@created_at,
@updated_at,
@standards,
@version
@standards
);
`
@@ -148,7 +263,6 @@ VALUES (
"organization_id": c.OrganizationID,
"category": c.Category,
"name": c.Name,
"version": 0,
"description": c.Description,
"content_ref": c.ContentRef,
"created_at": c.CreatedAt,
@@ -161,55 +275,6 @@ VALUES (
return err
}
func (c *Mitigations) LoadByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
organizationID gid.GID,
cursor *page.Cursor[MitigationOrderField],
) error {
q := `
SELECT
id,
organization_id,
category,
name,
description,
state,
importance,
content_ref,
created_at,
updated_at,
standards,
version
FROM
mitigations
WHERE
%s
AND organization_id = @organization_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query mitigations: %w", err)
}
mitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Mitigation])
if err != nil {
return fmt.Errorf("cannot collect mitigations: %w", err)
}
*c = mitigations
return nil
}
func (c *Mitigation) Update(
ctx context.Context,
conn pg.Conn,

View File

@@ -55,6 +55,67 @@ type (
}
)
func (s ControlService) ListForMitigationID(
ctx context.Context,
mitigationID gid.GID,
cursor *page.Cursor[coredata.ControlOrderField],
) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) {
var controls coredata.Controls
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controls.LoadByMitigationID(ctx, conn, s.svc.scope, mitigationID, cursor)
},
)
if err != nil {
return nil, fmt.Errorf("cannot list controls: %w", err)
}
return page.NewPage(controls, cursor), nil
}
func (s ControlService) CreateMapping(
ctx context.Context,
controlID gid.GID,
mitigationID gid.GID,
) error {
controlMitigation := &coredata.ControlMitigation{
ControlID: controlID,
MitigationID: mitigationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controlMitigation.Insert(ctx, conn, s.svc.scope)
},
)
}
func (s ControlService) DeleteMapping(
ctx context.Context,
controlID gid.GID,
mitigationID gid.GID,
) error {
controlMitigation := &coredata.ControlMitigation{
ControlID: controlID,
MitigationID: mitigationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controlMitigation.Delete(ctx, conn, s.svc.scope)
},
)
}
// Create creates a new control
func (s ControlService) Create(
ctx context.Context,

View File

@@ -57,6 +57,27 @@ type (
}
)
func (s MitigationService) ListForControlID(
ctx context.Context,
controlID gid.GID,
cursor *page.Cursor[coredata.MitigationOrderField],
) (*page.Page[*coredata.Mitigation, coredata.MitigationOrderField], error) {
var mitigations coredata.Mitigations
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return mitigations.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor)
},
)
if err != nil {
return nil, err
}
return page.NewPage(mitigations, cursor), nil
}
func (s MitigationService) Get(
ctx context.Context,
mitigationID gid.GID,

View File

@@ -441,6 +441,15 @@ type Control implements Node {
referenceId: String!
name: String!
description: String!
mitigations(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: MitigationOrder
): MitigationConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -461,6 +470,22 @@ type Mitigation implements Node {
orderBy: TaskOrder
): TaskConnection! @goField(forceResolver: true)
risks(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: RiskOrder
): RiskConnection! @goField(forceResolver: true)
controls(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: ControlOrder
): ControlConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -517,6 +542,15 @@ type Risk implements Node {
description: String!
probability: Float!
impact: Float!
controls(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: ControlOrder
): ControlConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -694,6 +728,14 @@ type Mutation {
updateMitigation(input: UpdateMitigationInput!): UpdateMitigationPayload!
importMitigation(input: ImportMitigationInput!): ImportMitigationPayload!
# Control mutations
createControlMapping(
input: CreateControlMappingInput!
): CreateControlMappingPayload!
deleteControlMapping(
input: DeleteControlMappingInput!
): DeleteControlMappingPayload!
# Task mutations
createTask(input: CreateTaskInput!): CreateTaskPayload!
updateTask(input: UpdateTaskInput!): UpdateTaskPayload!
@@ -853,6 +895,16 @@ input UnassignTaskInput {
taskId: ID!
}
input CreateControlMappingInput {
controlId: ID!
mitigationId: ID!
}
input DeleteControlMappingInput {
controlId: ID!
mitigationId: ID!
}
input CreateRiskInput {
organizationId: ID!
name: String!
@@ -1008,6 +1060,14 @@ type UnassignTaskPayload {
task: Task!
}
type CreateControlMappingPayload {
success: Boolean!
}
type DeleteControlMappingPayload {
success: Boolean!
}
type CreateRiskPayload {
riskEdge: RiskEdge!
}

File diff suppressed because it is too large Load Diff

View File

@@ -37,12 +37,13 @@ type ConfirmEmailPayload struct {
}
type Control struct {
ID gid.GID `json:"id"`
ReferenceID string `json:"referenceId"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
ReferenceID string `json:"referenceId"`
Name string `json:"name"`
Description string `json:"description"`
Mitigations *MitigationConnection `json:"mitigations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Control) IsNode() {}
@@ -58,6 +59,15 @@ type ControlEdge struct {
Node *Control `json:"node"`
}
type CreateControlMappingInput struct {
ControlID gid.GID `json:"controlId"`
MitigationID gid.GID `json:"mitigationId"`
}
type CreateControlMappingPayload struct {
Success bool `json:"success"`
}
type CreateFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -154,6 +164,15 @@ type CreateVendorPayload struct {
VendorEdge *VendorEdge `json:"vendorEdge"`
}
type DeleteControlMappingInput struct {
ControlID gid.GID `json:"controlId"`
MitigationID gid.GID `json:"mitigationId"`
}
type DeleteControlMappingPayload struct {
Success bool `json:"success"`
}
type DeleteEvidenceInput struct {
EvidenceID gid.GID `json:"evidenceId"`
}
@@ -303,6 +322,8 @@ type Mitigation struct {
State coredata.MitigationState `json:"state"`
Importance coredata.MitigationImportance `json:"importance"`
Tasks *TaskConnection `json:"tasks"`
Risks *RiskConnection `json:"risks"`
Controls *ControlConnection `json:"controls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
@@ -423,13 +444,14 @@ type RemoveUserPayload struct {
}
type Risk struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Probability float64 `json:"probability"`
Impact float64 `json:"impact"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Probability float64 `json:"probability"`
Impact float64 `json:"impact"`
Controls *ControlConnection `json:"controls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Risk) IsNode() {}

View File

@@ -19,6 +19,31 @@ import (
"github.com/vektah/gqlparser/v2/gqlerror"
)
// Mitigations is the resolver for the mitigations field.
func (r *controlResolver) Mitigations(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MitigationOrderBy) (*types.MitigationConnection, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.MitigationOrderField]{
Field: coredata.MitigationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.MitigationOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Mitigations.ListForControlID(ctx, obj.ID, cursor)
if err != nil {
return nil, fmt.Errorf("cannot list mitigations: %w", err)
}
return types.NewMitigationConnection(page), nil
}
// FileURL is the resolver for the fileUrl field.
func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*string, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
@@ -86,6 +111,36 @@ func (r *mitigationResolver) Tasks(ctx context.Context, obj *types.Mitigation, f
return types.NewTaskConnection(page), nil
}
// Risks is the resolver for the risks field.
func (r *mitigationResolver) Risks(ctx context.Context, obj *types.Mitigation, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy) (*types.RiskConnection, error) {
panic(fmt.Errorf("not implemented: Risks - risks"))
}
// Controls is the resolver for the controls field.
func (r *mitigationResolver) Controls(ctx context.Context, obj *types.Mitigation, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Controls.ListForMitigationID(ctx, obj.ID, cursor)
if err != nil {
return nil, fmt.Errorf("cannot list mitigation controls: %w", err)
}
return types.NewControlConnection(page), nil
}
// CreateOrganization is the resolver for the createOrganization field.
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
svc := r.proboSvc.WithTenant(gid.NewTenantID())
@@ -446,6 +501,34 @@ func (r *mutationResolver) ImportMitigation(ctx context.Context, input types.Imp
}, nil
}
// CreateControlMapping is the resolver for the createControlMapping field.
func (r *mutationResolver) CreateControlMapping(ctx context.Context, input types.CreateControlMappingInput) (*types.CreateControlMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
err := svc.Controls.CreateMapping(ctx, input.ControlID, input.MitigationID)
if err != nil {
return nil, fmt.Errorf("cannot create control mapping: %w", err)
}
return &types.CreateControlMappingPayload{
Success: true,
}, nil
}
// DeleteControlMapping is the resolver for the deleteControlMapping field.
func (r *mutationResolver) DeleteControlMapping(ctx context.Context, input types.DeleteControlMappingInput) (*types.DeleteControlMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
err := svc.Controls.DeleteMapping(ctx, input.ControlID, input.MitigationID)
if err != nil {
return nil, fmt.Errorf("cannot delete control mapping: %w", err)
}
return &types.DeleteControlMappingPayload{
Success: true,
}, nil
}
// CreateTask is the resolver for the createTask field.
func (r *mutationResolver) CreateTask(ctx context.Context, input types.CreateTaskInput) (*types.CreateTaskPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.MitigationID.TenantID())
@@ -983,6 +1066,11 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.Viewer, error) {
}, nil
}
// Controls is the resolver for the controls field.
func (r *riskResolver) Controls(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) {
panic(fmt.Errorf("not implemented: Controls - controls"))
}
// AssignedTo is the resolver for the assignedTo field.
func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.People, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
@@ -1053,6 +1141,9 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
}, nil
}
// Control returns schema.ControlResolver implementation.
func (r *Resolver) Control() schema.ControlResolver { return &controlResolver{r} }
// Evidence returns schema.EvidenceResolver implementation.
func (r *Resolver) Evidence() schema.EvidenceResolver { return &evidenceResolver{r} }
@@ -1074,12 +1165,16 @@ func (r *Resolver) Policy() schema.PolicyResolver { return &policyResolver{r} }
// Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
// Risk returns schema.RiskResolver implementation.
func (r *Resolver) Risk() schema.RiskResolver { return &riskResolver{r} }
// Task returns schema.TaskResolver implementation.
func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} }
// Viewer returns schema.ViewerResolver implementation.
func (r *Resolver) Viewer() schema.ViewerResolver { return &viewerResolver{r} }
type controlResolver struct{ *Resolver }
type evidenceResolver struct{ *Resolver }
type frameworkResolver struct{ *Resolver }
type mitigationResolver struct{ *Resolver }
@@ -1087,5 +1182,6 @@ type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type policyResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type riskResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type viewerResolver struct{ *Resolver }