First step of mitigation migration

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-03-27 23:34:35 +01:00
parent 449bd620da
commit 7321862ba9
62 changed files with 4025 additions and 2141 deletions

262
pkg/coredata/control.go Normal file
View File

@@ -0,0 +1,262 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
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"`
Name string `db:"name"`
Description string `db:"description"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Controls []*Control
UpdateControlParams struct {
ExpectedVersion int
Name *string
Description *string
}
)
func (c Control) CursorKey(orderBy ControlOrderField) page.CursorKey {
switch orderBy {
case ControlOrderFieldCreatedAt:
return page.CursorKey{ID: c.ID, Value: c.CreatedAt}
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (c *Controls) LoadByFrameworkID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
frameworkID gid.GID,
cursor *page.Cursor[ControlOrderField],
) error {
q := `
SELECT
id,
reference_id,
framework_id,
tenant_id,
name,
description,
created_at,
updated_at
FROM
controls
WHERE
%s
AND framework_id = @framework_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"framework_id": frameworkID}
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 *Control) LoadByID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
controlID gid.GID,
) error {
q := `
SELECT
id,
reference_id,
framework_id,
tenant_id,
name,
description,
created_at,
updated_at
FROM
controls
WHERE
%s
AND id = @control_id
LIMIT 1;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"control_id": controlID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query controls: %w", err)
}
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Control])
if err != nil {
return fmt.Errorf("cannot collect control: %w", err)
}
*c = control
return nil
}
func (c Control) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO
controls (
tenant_id,
id,
framework_id,
reference_id,
name,
description,
created_at,
updated_at
)
VALUES (
@tenant_id,
@control_id,
@framework_id,
@reference_id,
@name,
@description,
@created_at,
@updated_at
);
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"control_id": c.ID,
"framework_id": c.FrameworkID,
"reference_id": c.ReferenceID,
"name": c.Name,
"description": c.Description,
"created_at": c.CreatedAt,
"updated_at": c.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (c Control) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE
FROM
controls
WHERE
%s
AND id = @control_id;
`
args := pgx.StrictNamedArgs{"control_id": c.ID}
maps.Copy(args, scope.SQLArguments())
q = fmt.Sprintf(q, scope.SQLFragment())
_, err := conn.Exec(ctx, q, args)
return err
}
func (c *Control) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
params UpdateControlParams,
) error {
q := `
UPDATE controls SET
name = COALESCE(@name, name),
description = COALESCE(@description, description),
updated_at = @updated_at
WHERE %s
AND id = @control_id
RETURNING
id,
framework_id,
tenant_id,
name,
description,
created_at,
updated_at
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"control_id": c.ID,
"expected_version": params.ExpectedVersion,
"updated_at": time.Now(),
}
if params.Name != nil {
args["name"] = *params.Name
}
if params.Description != nil {
args["description"] = *params.Description
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query controls: %w", err)
}
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Control])
if err != nil {
return fmt.Errorf("cannot collect control: %w", err)
}
*c = control
return nil
}

View File

@@ -0,0 +1,168 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
ControlMitigation struct {
ControlID gid.GID `db:"control_id"`
MitigationID gid.GID `db:"mitigation_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
}
ControlMitigations []*ControlMitigation
)
func (cm ControlMitigation) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO
control_mitigations (
control_id,
mitigation_id,
tenant_id,
created_at
)
VALUES (
@control_id,
@mitigation_id,
@tenant_id,
@created_at
);
`
args := pgx.StrictNamedArgs{
"control_id": cm.ControlID,
"mitigation_id": cm.MitigationID,
"tenant_id": scope.GetTenantID(),
"created_at": cm.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (cm ControlMitigation) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE
FROM
control_mitigations
WHERE
%s
AND control_id = @control_id
AND mitigation_id = @mitigation_id;
`
args := pgx.StrictNamedArgs{
"control_id": cm.ControlID,
"mitigation_id": cm.MitigationID,
}
maps.Copy(args, scope.SQLArguments())
q = fmt.Sprintf(q, scope.SQLFragment())
_, err := conn.Exec(ctx, q, args)
return err
}
func (cms *ControlMitigations) LoadByMitigationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
mitigationID gid.GID,
) error {
q := `
SELECT
control_id,
mitigation_id,
tenant_id,
created_at
FROM
control_mitigations
WHERE
%s
AND mitigation_id = @mitigation_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"mitigation_id": mitigationID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query control_mitigations: %w", err)
}
controlMitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlMitigation])
if err != nil {
return fmt.Errorf("cannot collect control_mitigations: %w", err)
}
*cms = controlMitigations
return nil
}
func (cms *ControlMitigations) LoadByControlID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
controlID gid.GID,
) error {
q := `
SELECT
control_id,
mitigation_id,
tenant_id,
created_at
FROM
control_mitigations
WHERE
%s
AND control_id = @control_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"control_id": controlID}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query control_mitigations: %w", err)
}
controlMitigations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlMitigation])
if err != nil {
return fmt.Errorf("cannot collect control_mitigations: %w", err)
}
*cms = controlMitigations
return nil
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
type (
ControlOrderField string
)
const (
ControlOrderFieldCreatedAt ControlOrderField = "CREATED_AT"
)
func (p ControlOrderField) Column() string {
return string(p)
}
func (p ControlOrderField) String() string {
return string(p)
}
func (p ControlOrderField) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *ControlOrderField) UnmarshalText(text []byte) error {
*p = ControlOrderField(text)
return nil
}

View File

@@ -29,4 +29,5 @@ const (
UserEntityType
SessionEntityType
EmailEntityType
ControlEntityType
)

View File

@@ -30,27 +30,20 @@ type (
Framework struct {
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
ReferenceID string `db:"reference_id"`
Name string `db:"name"`
Description string `db:"description"`
ContentRef string `db:"content_ref"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
Version int `db:"version"`
}
Frameworks []*Framework
UpdateFrameworkParams struct {
ExpectedVersion int
Name *string
Description *string
}
)
func (f Framework) CursorKey(orderBy FrameworkOrderField) page.CursorKey {
func (f *Framework) CursorKey(orderBy FrameworkOrderField) page.CursorKey {
switch orderBy {
case FrameworkOrderFieldCreatedAt:
return page.NewCursorKey(f.ID, f.CreatedAt)
return page.CursorKey{ID: f.ID, Value: f.CreatedAt}
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
@@ -67,25 +60,23 @@ func (f *Frameworks) LoadByOrganizationID(
SELECT
id,
organization_id,
reference_id,
name,
description,
content_ref,
created_at,
updated_at,
version
updated_at
FROM
frameworks
WHERE
%s
AND organization_id = @organization_id
AND %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
@@ -112,12 +103,11 @@ func (f *Framework) LoadByID(
SELECT
id,
organization_id,
reference_id,
name,
description,
content_ref,
created_at,
updated_at,
version
updated_at
FROM
frameworks
WHERE
@@ -156,23 +146,21 @@ INSERT INTO
tenant_id,
id,
organization_id,
reference_id,
name,
description,
content_ref,
created_at,
updated_at,
version
updated_at
)
VALUES (
@tenant_id,
@framework_id,
@organization_id,
@reference_id,
@name,
@description,
@content_ref,
@created_at,
@updated_at,
@version
@updated_at
);
`
@@ -180,12 +168,11 @@ VALUES (
"tenant_id": scope.GetTenantID(),
"framework_id": f.ID,
"organization_id": f.OrganizationID,
"reference_id": f.ReferenceID,
"name": f.Name,
"description": f.Description,
"content_ref": f.ContentRef,
"created_at": f.CreatedAt,
"updated_at": f.UpdatedAt,
"version": f.Version,
}
_, err := conn.Exec(ctx, q, args)
return err
@@ -217,55 +204,28 @@ func (f *Framework) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
params UpdateFrameworkParams,
) error {
q := `
UPDATE frameworks SET
name = COALESCE(@name, name),
description = COALESCE(@description, description),
updated_at = @updated_at,
version = version + 1
WHERE %s
AND id = @framework_id
AND version = @expected_version
RETURNING
id,
organization_id,
name,
description,
content_ref,
created_at,
updated_at,
version
UPDATE frameworks
SET
name = @name,
description = @description,
updated_at = @updated_at
WHERE
%s
AND id = @framework_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"framework_id": f.ID,
"expected_version": params.ExpectedVersion,
"updated_at": time.Now(),
}
if params.Name != nil {
args["name"] = *params.Name
}
if params.Description != nil {
args["description"] = *params.Description
"framework_id": f.ID,
"updated_at": f.UpdatedAt,
"name": f.Name,
"description": f.Description,
}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query frameworks: %w", err)
}
framework, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[Framework])
if err != nil {
return fmt.Errorf("cannot collect framework: %w", err)
}
*f = framework
return nil
_, err := conn.Exec(ctx, q, args)
return err
}

View File

@@ -0,0 +1,18 @@
-- Add organization_id column to mitigations table
ALTER TABLE mitigations ADD COLUMN organization_id TEXT;
-- Update mitigations to set organization_id based on framework's organization_id
UPDATE mitigations m
SET organization_id = f.organization_id
FROM frameworks f
WHERE m.framework_id = f.id;
-- Make organization_id NOT NULL after update
ALTER TABLE mitigations ALTER COLUMN organization_id SET NOT NULL;
-- Add foreign key constraint
ALTER TABLE mitigations ADD CONSTRAINT fk_mitigations_organization_id
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
-- Add index for performance
CREATE INDEX idx_mitigations_organization_id ON mitigations(organization_id);

View File

@@ -0,0 +1,8 @@
-- Drop the foreign key constraint first
ALTER TABLE mitigations DROP CONSTRAINT IF EXISTS fk_mitigations_framework_id;
-- Drop any indices on framework_id
DROP INDEX IF EXISTS idx_mitigations_framework_id;
-- Remove the framework_id column
ALTER TABLE mitigations DROP COLUMN framework_id;

View File

@@ -0,0 +1 @@
ALTER TABLE frameworks ADD COLUMN reference_id TEXT;

View File

@@ -0,0 +1,2 @@
ALTER TABLE controls ADD COLUMN reference_id TEXT NOT NULL DEFAULT '';
ALTER TABLE controls ALTER COLUMN reference_id DROP DEFAULT;

View File

@@ -0,0 +1 @@
ALTER TABLE frameworks DROP COLUMN content_ref;

View File

@@ -0,0 +1 @@
ALTER TABLE frameworks DROP COLUMN version;

View File

@@ -0,0 +1,19 @@
-- Create a new table for controls with string IDs
CREATE TABLE controls (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
framework_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
version INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE controls_mitigations (
control_id TEXT NOT NULL,
mitigation_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
PRIMARY KEY (control_id, mitigation_id)
);

View File

@@ -29,18 +29,18 @@ import (
type (
Mitigation struct {
ID gid.GID `db:"id"`
FrameworkID gid.GID `db:"framework_id"`
Category string `db:"category"`
Name string `db:"name"`
Description string `db:"description"`
Importance MitigationImportance `db:"importance"`
State MitigationState `db:"state"`
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"`
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Category string `db:"category"`
Name string `db:"name"`
Description string `db:"description"`
Importance MitigationImportance `db:"importance"`
State MitigationState `db:"state"`
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"`
}
Mitigations []*Mitigation
@@ -73,7 +73,7 @@ func (c *Mitigation) LoadByID(
q := `
SELECT
id,
framework_id,
organization_id,
category,
name,
description,
@@ -122,7 +122,7 @@ INSERT INTO
mitigations (
tenant_id,
id,
framework_id,
organization_id,
category,
name,
importance,
@@ -137,7 +137,7 @@ INSERT INTO
VALUES (
@tenant_id,
@mitigation_id,
@framework_id,
@organization_id,
@category,
@name,
@importance,
@@ -152,35 +152,35 @@ VALUES (
`
args := pgx.StrictNamedArgs{
"tenant_id": scope.GetTenantID(),
"mitigation_id": c.ID,
"framework_id": c.FrameworkID,
"category": c.Category,
"name": c.Name,
"version": 0,
"description": c.Description,
"content_ref": c.ContentRef,
"created_at": c.CreatedAt,
"updated_at": c.UpdatedAt,
"state": c.State,
"importance": c.Importance,
"standards": c.Standards,
"tenant_id": scope.GetTenantID(),
"mitigation_id": c.ID,
"organization_id": c.OrganizationID,
"category": c.Category,
"name": c.Name,
"version": 0,
"description": c.Description,
"content_ref": c.ContentRef,
"created_at": c.CreatedAt,
"updated_at": c.UpdatedAt,
"state": c.State,
"importance": c.Importance,
"standards": c.Standards,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (c *Mitigations) LoadByFrameworkID(
func (c *Mitigations) LoadByOrganizationID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
frameworkID gid.GID,
organizationID gid.GID,
cursor *page.Cursor[MitigationOrderField],
) error {
q := `
SELECT
id,
framework_id,
organization_id,
category,
name,
description,
@@ -195,12 +195,12 @@ FROM
mitigations
WHERE
%s
AND framework_id = @framework_id
AND organization_id = @organization_id
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"framework_id": frameworkID}
args := pgx.StrictNamedArgs{"organization_id": organizationID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
@@ -239,7 +239,7 @@ WHERE %s
AND version = @expected_version
RETURNING
id,
framework_id,
organization_id,
category,
name,
description,
@@ -248,8 +248,8 @@ RETURNING
content_ref,
created_at,
updated_at,
version,
standards
standards,
version
`
q = fmt.Sprintf(q, scope.SQLFragment())

View File

@@ -27,6 +27,12 @@ type CursorKey struct {
Value any
}
// StringCursorKey is a cursor key for string IDs
type StringCursorKey struct {
ID string
Value any
}
var (
CursorKeyNil CursorKey

View File

@@ -0,0 +1,282 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package probo
import (
"context"
"fmt"
"time"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"go.gearno.de/kit/pg"
)
type (
ControlService struct {
svc *TenantService
}
CreateControlRequest struct {
ID gid.GID
FrameworkID gid.GID
Name string
Description string
}
UpdateControlRequest struct {
ID gid.GID
ExpectedVersion int
Name *string
Description *string
}
ConnectControlToMitigationRequest struct {
ControlID gid.GID
MitigationID gid.GID
}
DisconnectControlFromMitigationRequest struct {
ControlID gid.GID
MitigationID gid.GID
}
)
// Create creates a new control
func (s ControlService) Create(
ctx context.Context,
req CreateControlRequest,
) (*coredata.Control, error) {
now := time.Now()
control := &coredata.Control{
ID: req.ID,
FrameworkID: req.FrameworkID,
TenantID: s.svc.scope.GetTenantID(),
Name: req.Name,
Description: req.Description,
CreatedAt: now,
UpdatedAt: now,
}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return control.Insert(ctx, conn, s.svc.scope)
},
)
if err != nil {
return nil, fmt.Errorf("cannot create control: %w", err)
}
return control, nil
}
// Get retrieves a control by ID
func (s ControlService) Get(
ctx context.Context,
controlID gid.GID,
) (*coredata.Control, error) {
control := &coredata.Control{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return control.LoadByID(ctx, conn, s.svc.scope, controlID)
},
)
if err != nil {
return nil, fmt.Errorf("cannot get control: %w", err)
}
return control, nil
}
// Update updates an existing control
func (s ControlService) Update(
ctx context.Context,
req UpdateControlRequest,
) (*coredata.Control, error) {
params := coredata.UpdateControlParams{
ExpectedVersion: req.ExpectedVersion,
Name: req.Name,
Description: req.Description,
}
control := &coredata.Control{ID: req.ID}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
return control.Update(ctx, conn, s.svc.scope, params)
})
if err != nil {
return nil, fmt.Errorf("cannot update control: %w", err)
}
return control, nil
}
// Delete removes a control
func (s ControlService) Delete(
ctx context.Context,
controlID gid.GID,
) error {
control := &coredata.Control{ID: controlID}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return control.Delete(ctx, conn, s.svc.scope)
},
)
}
// ListForFrameworkID retrieves all controls for a framework
func (s ControlService) ListForFrameworkID(
ctx context.Context,
frameworkID 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.LoadByFrameworkID(
ctx,
conn,
s.svc.scope,
frameworkID,
cursor,
)
},
)
if err != nil {
return nil, fmt.Errorf("cannot list controls: %w", err)
}
return page.NewPage(controls, cursor), nil
}
func (s ControlService) ConnectToMitigation(
ctx context.Context,
req ConnectControlToMitigationRequest,
) error {
now := time.Now()
controlMitigation := &coredata.ControlMitigation{
ControlID: req.ControlID,
MitigationID: req.MitigationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: now,
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controlMitigation.Insert(ctx, conn, s.svc.scope)
},
)
}
// DisconnectFromMitigation removes the link between a control and a mitigation
func (s ControlService) DisconnectFromMitigation(
ctx context.Context,
req DisconnectControlFromMitigationRequest,
) error {
controlMitigation := &coredata.ControlMitigation{
ControlID: req.ControlID,
MitigationID: req.MitigationID,
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controlMitigation.Delete(ctx, conn, s.svc.scope)
},
)
}
func (s ControlService) ListMitigationsForControlID(
ctx context.Context,
controlID gid.GID,
) ([]*coredata.Mitigation, error) {
var controlMitigations coredata.ControlMitigations
var mitigations []*coredata.Mitigation
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := controlMitigations.LoadByControlID(ctx, conn, s.svc.scope, controlID); err != nil {
return fmt.Errorf("cannot load control mitigations: %w", err)
}
for _, cm := range controlMitigations {
mitigation := &coredata.Mitigation{}
if err := mitigation.LoadByID(ctx, conn, s.svc.scope, cm.MitigationID); err != nil {
return fmt.Errorf("cannot load mitigation: %w", err)
}
mitigations = append(mitigations, mitigation)
}
return nil
},
)
if err != nil {
return nil, err
}
return mitigations, nil
}
// ListControlsForMitigationID retrieves all controls linked to a mitigation
func (s ControlService) ListControlsForMitigationID(
ctx context.Context,
mitigationID gid.GID,
) ([]*coredata.Control, error) {
var controlMitigations coredata.ControlMitigations
var controls []*coredata.Control
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := controlMitigations.LoadByMitigationID(ctx, conn, s.svc.scope, mitigationID); err != nil {
return fmt.Errorf("cannot load control mitigations: %w", err)
}
for _, cm := range controlMitigations {
control := &coredata.Control{}
if err := control.LoadByID(ctx, conn, s.svc.scope, cm.ControlID); err != nil {
return fmt.Errorf("cannot load control: %w", err)
}
controls = append(controls, control)
}
return nil
},
)
if err != nil {
return nil, err
}
return controls, nil
}

View File

@@ -19,7 +19,6 @@ import (
"fmt"
"time"
"gearno.de/ref"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
@@ -34,38 +33,22 @@ type (
CreateFrameworkRequest struct {
OrganizationID gid.GID
Name string
Description string
ContentRef string
}
UpdateFrameworkRequest struct {
ID gid.GID
ExpectedVersion int
Name *string
Description *string
ID gid.GID
Name *string
Description *string
}
ImportFrameworkRequest struct {
Data struct {
Framework struct {
Framework struct {
Name string `json:"name"`
Controls []struct {
ID string `json:"id"`
Name string `json:"name"`
ContentRef string `json:"content-ref"`
Description string `json:"description"`
Version string `json:"version"`
Controls []struct {
ContentRef string `json:"content-ref"`
Category string `json:"category"`
Importance coredata.MitigationImportance `json:"importance"`
Standards []string `json:"standards"`
Name string `json:"name"`
Description string `json:"description"`
Tasks []struct {
Name string `json:"name"`
Description string `json:"description"`
TimeEstimate int `json:"time-estimate"`
} `json:"tasks"`
} `json:"controls"`
} `json:"framework"`
} `json:"controls"`
}
}
)
@@ -84,8 +67,6 @@ func (s FrameworkService) Create(
ID: frameworkID,
OrganizationID: req.OrganizationID,
Name: req.Name,
Description: req.Description,
ContentRef: req.ContentRef,
CreatedAt: now,
UpdatedAt: now,
}
@@ -155,19 +136,26 @@ func (s FrameworkService) Update(
ctx context.Context,
req UpdateFrameworkRequest,
) (*coredata.Framework, error) {
params := coredata.UpdateFrameworkParams{
ExpectedVersion: req.ExpectedVersion,
Name: req.Name,
Description: req.Description,
}
framework := &coredata.Framework{ID: req.ID}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
return framework.Update(ctx, conn, s.svc.scope, params)
})
if err := framework.LoadByID(ctx, conn, s.svc.scope, req.ID); err != nil {
return fmt.Errorf("cannot load framework: %w", err)
}
if req.Name != nil {
framework.Name = *req.Name
}
if req.Description != nil {
framework.Description = *req.Description
}
return framework.Update(ctx, conn, s.svc.scope)
},
)
if err != nil {
return nil, err
}
@@ -194,70 +182,41 @@ func (s FrameworkService) Import(
organizationID gid.GID,
req ImportFrameworkRequest,
) (*coredata.Framework, error) {
now := time.Now()
frameworkID, err := gid.NewGID(organizationID.TenantID(), coredata.FrameworkEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create global id: %w", err)
}
now := time.Now()
framework := &coredata.Framework{
ID: frameworkID,
OrganizationID: organizationID,
Name: req.Data.Framework.Name,
Description: req.Data.Framework.Description,
ContentRef: req.Data.Framework.ContentRef,
ReferenceID: req.Framework.Name,
Name: req.Framework.Name,
CreatedAt: now,
UpdatedAt: now,
}
importedMitigations := coredata.Mitigations{}
importedTasks := coredata.Tasks{}
for _, mitigation := range req.Data.Framework.Controls {
controlID, err := gid.NewGID(organizationID.TenantID(), coredata.MitigationEntityType)
importedControls := coredata.Controls{}
for _, control := range req.Framework.Controls {
controlID, err := gid.NewGID(organizationID.TenantID(), coredata.ControlEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create global id: %w", err)
}
importedControl := &coredata.Mitigation{
now := time.Now()
control := &coredata.Control{
ID: controlID,
TenantID: organizationID.TenantID(),
FrameworkID: frameworkID,
Category: mitigation.Category,
Importance: coredata.MitigationImportance(mitigation.Importance),
Name: mitigation.Name,
Description: mitigation.Description,
State: coredata.MitigationStateNotStarted,
ContentRef: mitigation.ContentRef,
ReferenceID: control.ID,
Name: control.Name,
Description: control.Description,
CreatedAt: now,
UpdatedAt: now,
Standards: mitigation.Standards,
}
importedMitigations = append(importedMitigations, importedControl)
for _, task := range mitigation.Tasks {
taskID, err := gid.NewGID(organizationID.TenantID(), coredata.TaskEntityType)
if err != nil {
return nil, fmt.Errorf("cannot create global id: %w", err)
}
var timeEstimate *time.Duration
if task.TimeEstimate > 0 {
timeEstimate = ref.Ref(time.Duration(task.TimeEstimate) * time.Second)
}
importedTasks = append(importedTasks, &coredata.Task{
ID: taskID,
MitigationID: controlID,
Name: task.Name,
State: coredata.TaskStateTodo,
Description: task.Description,
CreatedAt: now,
UpdatedAt: now,
TimeEstimate: timeEstimate,
})
}
importedControls = append(importedControls, control)
}
err = s.svc.pg.WithTx(
@@ -269,15 +228,9 @@ func (s FrameworkService) Import(
return fmt.Errorf("cannot insert framework: %w", err)
}
for _, importedMitigation := range importedMitigations {
if err := importedMitigation.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert mitigation: %w", err)
}
}
for _, importedTask := range importedTasks {
if err := importedTask.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert task: %w", err)
for _, importedControl := range importedControls {
if err := importedControl.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert control: %w", err)
}
}

View File

@@ -31,11 +31,11 @@ type (
}
CreateMitigationRequest struct {
FrameworkID gid.GID
Name string
Description string
Category string
Importance coredata.MitigationImportance
OrganizationID gid.GID
Name string
Description string
Category string
Importance coredata.MitigationImportance
}
UpdateMitigationRequest struct {
@@ -96,9 +96,9 @@ func (s MitigationService) Update(
return mitigation, nil
}
func (s MitigationService) ListForFrameworkID(
func (s MitigationService) ListForOrganizationID(
ctx context.Context,
frameworkID gid.GID,
organizationID gid.GID,
cursor *page.Cursor[coredata.MitigationOrderField],
) (*page.Page[*coredata.Mitigation, coredata.MitigationOrderField], error) {
var mitigations coredata.Mitigations
@@ -106,11 +106,11 @@ func (s MitigationService) ListForFrameworkID(
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return mitigations.LoadByFrameworkID(
return mitigations.LoadByOrganizationID(
ctx,
conn,
s.svc.scope,
frameworkID,
organizationID,
cursor,
)
},
@@ -133,27 +133,22 @@ func (s MitigationService) Create(
return nil, fmt.Errorf("cannot create mitigation global id: %w", err)
}
framework := &coredata.Framework{}
mitigation := &coredata.Mitigation{
ID: mitigationID,
FrameworkID: req.FrameworkID,
Name: req.Name,
Description: req.Description,
Category: req.Category,
State: coredata.MitigationStateNotStarted,
Standards: []string{},
Importance: req.Importance,
CreatedAt: now,
UpdatedAt: now,
ID: mitigationID,
OrganizationID: req.OrganizationID,
Name: req.Name,
Description: req.Description,
Category: req.Category,
State: coredata.MitigationStateNotStarted,
Standards: []string{},
Importance: req.Importance,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := framework.LoadByID(ctx, conn, s.svc.scope, req.FrameworkID); err != nil {
return fmt.Errorf("cannot load framework %q: %w", req.FrameworkID, err)
}
if err := mitigation.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert mitigation: %w", err)
}

View File

@@ -32,18 +32,21 @@ type (
}
TenantService struct {
pg *pg.Client
s3 *s3.Client
bucket string
scope coredata.Scoper
pg *pg.Client
s3 *s3.Client
bucket string
scope coredata.Scoper
Frameworks *FrameworkService
Mitigations *MitigationService
Tasks *TaskService
Evidences *EvidenceService
Peoples *PeopleService
Vendors *VendorService
Policies *PolicyService
Organizations *OrganizationService
Vendors *VendorService
Peoples *PeopleService
Policies *PolicyService
Controls *ControlService
}
)
@@ -82,5 +85,7 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
tenantService.Vendors = &VendorService{svc: tenantService}
tenantService.Policies = &PolicyService{svc: tenantService}
tenantService.Organizations = &OrganizationService{svc: tenantService}
tenantService.Controls = &ControlService{svc: tenantService}
return tenantService
}

View File

@@ -155,6 +155,14 @@ type Organization implements Node {
orderBy: PolicyOrder
): PolicyConnection! @goField(forceResolver: true)
mitigations(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: MitigationOrder
): MitigationConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -186,7 +194,18 @@ enum FrameworkOrderField
@goModel(
model: "github.com/getprobo/probo/pkg/coredata.FrameworkOrderField"
) {
NAME
CREATED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.FrameworkOrderFieldCreatedAt"
)
}
enum ControlOrderField
@goModel(model: "github.com/getprobo/probo/pkg/coredata.ControlOrderField") {
CREATED_AT
@goEnum(
value: "github.com/getprobo/probo/pkg/coredata.ControlOrderFieldCreatedAt"
)
}
enum MitigationOrderField
@@ -235,6 +254,14 @@ input FrameworkOrder
field: FrameworkOrderField!
}
input ControlOrder
@goModel(
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.ControlOrderBy"
) {
direction: OrderDirection!
field: ControlOrderField!
}
input MitigationOrder
@goModel(
model: "github.com/getprobo/probo/pkg/server/api/console/v1/types.MitigationOrderBy"
@@ -285,7 +312,6 @@ type People implements Node {
kind: PeopleKind!
createdAt: Datetime!
updatedAt: Datetime!
version: Int!
}
type VendorConnection {
@@ -311,7 +337,6 @@ type Vendor implements Node {
privacyPolicyUrl: String
createdAt: Datetime!
updatedAt: Datetime!
version: Int!
}
type FrameworkConnection {
@@ -326,23 +351,40 @@ type FrameworkEdge {
type Framework implements Node {
id: ID!
version: Int!
name: String!
description: String!
mitigations(
controls(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: MitigationOrder
): MitigationConnection! @goField(forceResolver: true)
orderBy: ControlOrder
): ControlConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type ControlConnection {
edges: [ControlEdge!]!
pageInfo: PageInfo!
}
type ControlEdge {
cursor: CursorKey!
node: Control!
}
type Control implements Node {
id: ID!
referenceId: String!
name: String!
description: String!
createdAt: Datetime!
updatedAt: Datetime!
}
type MitigationConnection {
edges: [MitigationEdge!]!
pageInfo: PageInfo!
@@ -355,7 +397,6 @@ type MitigationEdge {
type Mitigation implements Node {
id: ID!
version: Int!
category: String!
name: String!
description: String!
@@ -386,7 +427,6 @@ type TaskEdge {
type Task implements Node {
id: ID!
version: Int!
name: String!
description: String!
state: TaskState!
@@ -506,6 +546,7 @@ type Mutation {
createFramework(input: CreateFrameworkInput!): CreateFrameworkPayload!
updateFramework(input: UpdateFrameworkInput!): UpdateFrameworkPayload!
importFramework(input: ImportFrameworkInput!): ImportFrameworkPayload!
deleteFramework(input: DeleteFrameworkInput!): DeleteFrameworkPayload!
createMitigation(input: CreateMitigationInput!): CreateMitigationPayload!
updateMitigation(input: UpdateMitigationInput!): UpdateMitigationPayload!
@@ -553,7 +594,6 @@ input CreatePeopleInput {
input UpdatePeopleInput {
id: ID!
expectedVersion: Int!
fullName: String
primaryEmailAddress: String
additionalEmailAddresses: [String!]
@@ -588,7 +628,6 @@ enum RiskTier
input UpdateVendorInput {
id: ID!
expectedVersion: Int!
name: String
description: String
serviceStartAt: Datetime
@@ -662,6 +701,14 @@ type DeleteTaskPayload {
deletedTaskId: ID!
}
input DeleteFrameworkInput {
frameworkId: ID!
}
type DeleteFrameworkPayload {
deletedFrameworkId: ID!
}
input CreateFrameworkInput {
organizationId: ID!
name: String!
@@ -670,7 +717,6 @@ input CreateFrameworkInput {
input UpdateFrameworkInput {
id: ID!
expectedVersion: Int!
name: String
description: String
}
@@ -680,7 +726,7 @@ type CreateFrameworkPayload {
}
input CreateMitigationInput {
frameworkId: ID!
organizationId: ID!
name: String!
description: String!
category: String!
@@ -705,7 +751,6 @@ type UpdatePeoplePayload {
input UpdateMitigationInput {
id: ID!
expectedVersion: Int!
name: String
description: String
category: String
@@ -757,7 +802,6 @@ input CreatePolicyInput {
input UpdatePolicyInput {
id: ID!
expectedVersion: Int!
name: String
content: String
status: PolicyStatus
@@ -783,7 +827,6 @@ type DeletePolicyPayload {
type Policy implements Node {
id: ID!
version: Int!
name: String!
status: PolicyStatus!
content: String!
@@ -805,7 +848,6 @@ type PolicyEdge {
input UpdateTaskInput {
taskId: ID!
expectedVersion: Int!
name: String
description: String
state: TaskState

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/page"
)
type (
ControlOrderBy OrderBy[coredata.ControlOrderField]
)
func NewControlConnection(p *page.Page[*coredata.Control, coredata.ControlOrderField]) *ControlConnection {
var edges = make([]*ControlEdge, len(p.Data))
for i := range edges {
edges[i] = NewControlEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &ControlConnection{
Edges: edges,
PageInfo: NewPageInfo(p),
}
}
func NewControlEdge(c *coredata.Control, orderBy coredata.ControlOrderField) *ControlEdge {
return &ControlEdge{
Cursor: c.CursorKey(orderBy),
Node: NewControl(c),
}
}
func NewControl(c *coredata.Control) *Control {
return &Control{
ID: c.ID,
ReferenceID: c.ReferenceID,
Name: c.Name,
Description: c.Description,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}

View File

@@ -46,7 +46,6 @@ func NewFrameworkEdge(f *coredata.Framework, orderBy coredata.FrameworkOrderFiel
func NewFramework(f *coredata.Framework) *Framework {
return &Framework{
ID: f.ID,
Version: f.Version,
Name: f.Name,
Description: f.Description,
CreatedAt: f.CreatedAt,

View File

@@ -46,7 +46,6 @@ func NewMitigationEdge(c *coredata.Mitigation, orderBy coredata.MitigationOrderF
func NewMitigation(c *coredata.Mitigation) *Mitigation {
return &Mitigation{
ID: c.ID,
Version: c.Version,
Category: c.Category,
Name: c.Name,
Description: c.Description,

View File

@@ -52,6 +52,5 @@ func NewPeople(p *coredata.People) *People {
Kind: p.Kind,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
Version: p.Version,
}
}

View File

@@ -45,7 +45,6 @@ func NewPolicyEdge(policy *coredata.Policy, orderBy coredata.PolicyOrderField) *
func NewPolicy(policy *coredata.Policy) *Policy {
return &Policy{
ID: policy.ID,
Version: policy.Version,
Name: policy.Name,
Content: policy.Content,
CreatedAt: policy.CreatedAt,

View File

@@ -52,6 +52,5 @@ func NewTask(t *coredata.Task) *Task {
TimeEstimate: t.TimeEstimate,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
Version: t.Version,
}
}

View File

@@ -36,6 +36,28 @@ type ConfirmEmailPayload struct {
Success bool `json:"success"`
}
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"`
}
func (Control) IsNode() {}
func (this Control) GetID() gid.GID { return this.ID }
type ControlConnection struct {
Edges []*ControlEdge `json:"edges"`
PageInfo *PageInfo `json:"pageInfo"`
}
type ControlEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *Control `json:"node"`
}
type CreateFrameworkInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -47,11 +69,11 @@ type CreateFrameworkPayload struct {
}
type CreateMitigationInput struct {
FrameworkID gid.GID `json:"frameworkId"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Importance coredata.MitigationImportance `json:"importance"`
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Importance coredata.MitigationImportance `json:"importance"`
}
type CreateMitigationPayload struct {
@@ -128,6 +150,14 @@ type DeleteEvidencePayload struct {
DeletedEvidenceID gid.GID `json:"deletedEvidenceId"`
}
type DeleteFrameworkInput struct {
FrameworkID gid.GID `json:"frameworkId"`
}
type DeleteFrameworkPayload struct {
DeletedFrameworkID gid.GID `json:"deletedFrameworkId"`
}
type DeleteOrganizationInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
@@ -196,13 +226,12 @@ type EvidenceEdge struct {
}
type Framework struct {
ID gid.GID `json:"id"`
Version int `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
Mitigations *MitigationConnection `json:"mitigations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Controls *ControlConnection `json:"controls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Framework) IsNode() {}
@@ -239,7 +268,6 @@ type InviteUserPayload struct {
type Mitigation struct {
ID gid.GID `json:"id"`
Version int `json:"version"`
Category string `json:"category"`
Name string `json:"name"`
Description string `json:"description"`
@@ -267,16 +295,17 @@ type Mutation struct {
}
type Organization struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
LogoURL *string `json:"logoUrl,omitempty"`
Users *UserConnection `json:"users"`
Frameworks *FrameworkConnection `json:"frameworks"`
Vendors *VendorConnection `json:"vendors"`
Peoples *PeopleConnection `json:"peoples"`
Policies *PolicyConnection `json:"policies"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
Name string `json:"name"`
LogoURL *string `json:"logoUrl,omitempty"`
Users *UserConnection `json:"users"`
Frameworks *FrameworkConnection `json:"frameworks"`
Vendors *VendorConnection `json:"vendors"`
Peoples *PeopleConnection `json:"peoples"`
Policies *PolicyConnection `json:"policies"`
Mitigations *MitigationConnection `json:"mitigations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Organization) IsNode() {}
@@ -312,7 +341,6 @@ type People struct {
Kind coredata.PeopleKind `json:"kind"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Version int `json:"version"`
}
func (People) IsNode() {}
@@ -330,7 +358,6 @@ type PeopleEdge struct {
type Policy struct {
ID gid.GID `json:"id"`
Version int `json:"version"`
Name string `json:"name"`
Status coredata.PolicyStatus `json:"status"`
Content string `json:"content"`
@@ -372,7 +399,6 @@ type Session struct {
type Task struct {
ID gid.GID `json:"id"`
Version int `json:"version"`
Name string `json:"name"`
Description string `json:"description"`
State coredata.TaskState `json:"state"`
@@ -405,10 +431,9 @@ type UnassignTaskPayload struct {
}
type UpdateFrameworkInput struct {
ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
type UpdateFrameworkPayload struct {
@@ -416,13 +441,12 @@ type UpdateFrameworkPayload struct {
}
type UpdateMitigationInput struct {
ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty"`
State *coredata.MitigationState `json:"state,omitempty"`
Importance *coredata.MitigationImportance `json:"importance,omitempty"`
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Category *string `json:"category,omitempty"`
State *coredata.MitigationState `json:"state,omitempty"`
Importance *coredata.MitigationImportance `json:"importance,omitempty"`
}
type UpdateMitigationPayload struct {
@@ -441,7 +465,6 @@ type UpdateOrganizationPayload struct {
type UpdatePeopleInput struct {
ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
FullName *string `json:"fullName,omitempty"`
PrimaryEmailAddress *string `json:"primaryEmailAddress,omitempty"`
AdditionalEmailAddresses []string `json:"additionalEmailAddresses,omitempty"`
@@ -453,13 +476,12 @@ type UpdatePeoplePayload struct {
}
type UpdatePolicyInput struct {
ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"`
Content *string `json:"content,omitempty"`
Status *coredata.PolicyStatus `json:"status,omitempty"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Content *string `json:"content,omitempty"`
Status *coredata.PolicyStatus `json:"status,omitempty"`
ReviewDate *time.Time `json:"reviewDate,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
}
type UpdatePolicyPayload struct {
@@ -467,12 +489,11 @@ type UpdatePolicyPayload struct {
}
type UpdateTaskInput struct {
TaskID gid.GID `json:"taskId"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
State *coredata.TaskState `json:"state,omitempty"`
TimeEstimate *time.Duration `json:"timeEstimate,omitempty"`
TaskID gid.GID `json:"taskId"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
State *coredata.TaskState `json:"state,omitempty"`
TimeEstimate *time.Duration `json:"timeEstimate,omitempty"`
}
type UpdateTaskPayload struct {
@@ -481,7 +502,6 @@ type UpdateTaskPayload struct {
type UpdateVendorInput struct {
ID gid.GID `json:"id"`
ExpectedVersion int `json:"expectedVersion"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
ServiceStartAt *time.Time `json:"serviceStartAt,omitempty"`
@@ -544,7 +564,6 @@ type Vendor struct {
PrivacyPolicyURL *string `json:"privacyPolicyUrl,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Version int `json:"version"`
}
func (Vendor) IsNode() {}

View File

@@ -57,6 +57,5 @@ func NewVendor(v *coredata.Vendor) *Vendor {
StatusPageURL: v.StatusPageURL,
TermsOfServiceURL: v.TermsOfServiceURL,
PrivacyPolicyURL: v.PrivacyPolicyURL,
Version: v.Version,
}
}

View File

@@ -36,28 +36,29 @@ func (r *evidenceResolver) FileURL(ctx context.Context, obj *types.Evidence) (*s
return &result, nil
}
// Mitigations is the resolver for the mitigations field.
func (r *frameworkResolver) Mitigations(ctx context.Context, obj *types.Framework, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MitigationOrderBy) (*types.MitigationConnection, error) {
// Controls is the resolver for the controls field.
func (r *frameworkResolver) Controls(ctx context.Context, obj *types.Framework, 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.MitigationOrderField]{
Field: coredata.MitigationOrderFieldCreatedAt,
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.MitigationOrderField]{
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Mitigations.ListForFrameworkID(ctx, obj.ID, cursor)
page, err := svc.Controls.ListForFrameworkID(ctx, obj.ID, cursor)
if err != nil {
return nil, fmt.Errorf("cannot list framework mitigations: %w", err)
return nil, fmt.Errorf("cannot list controls: %w", err)
}
return types.NewMitigationConnection(page), nil
return types.NewControlConnection(page), nil
}
// Tasks is the resolver for the tasks field.
@@ -115,7 +116,6 @@ func (r *mutationResolver) UpdateVendor(ctx context.Context, input types.UpdateV
vendor, err := svc.Vendors.Update(ctx, probo.UpdateVendorRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
Name: input.Name,
Description: input.Description,
ServiceStartAt: input.ServiceStartAt,
@@ -176,7 +176,6 @@ func (r *mutationResolver) UpdatePeople(ctx context.Context, input types.UpdateP
people, err := svc.Peoples.Update(ctx, probo.UpdatePeopleRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
FullName: input.FullName,
PrimaryEmailAddress: input.PrimaryEmailAddress,
AdditionalEmailAddresses: &input.AdditionalEmailAddresses,
@@ -281,12 +280,11 @@ func (r *mutationResolver) UpdateTask(ctx context.Context, input types.UpdateTas
svc := r.GetTenantServiceIfAuthorized(ctx, input.TaskID.TenantID())
task, err := svc.Tasks.Update(ctx, probo.UpdateTaskRequest{
TaskID: input.TaskID,
ExpectedVersion: input.ExpectedVersion,
Name: input.Name,
Description: input.Description,
State: input.State,
TimeEstimate: input.TimeEstimate,
TaskID: input.TaskID,
Name: input.Name,
Description: input.Description,
State: input.State,
TimeEstimate: input.TimeEstimate,
})
if err != nil {
return nil, fmt.Errorf("cannot update task: %w", err)
@@ -346,7 +344,6 @@ func (r *mutationResolver) CreateFramework(ctx context.Context, input types.Crea
framework, err := svc.Frameworks.Create(ctx, probo.CreateFrameworkRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: input.Description,
})
if err != nil {
return nil, fmt.Errorf("cannot create framework: %w", err)
@@ -362,10 +359,9 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda
svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID())
framework, err := svc.Frameworks.Update(ctx, probo.UpdateFrameworkRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
Name: input.Name,
Description: input.Description,
ID: input.ID,
Name: input.Name,
Description: input.Description,
})
if err != nil {
return nil, fmt.Errorf("cannot update framework: %w", err)
@@ -376,12 +372,26 @@ func (r *mutationResolver) UpdateFramework(ctx context.Context, input types.Upda
}, nil
}
// DeleteFramework is the resolver for the deleteFramework field.
func (r *mutationResolver) DeleteFramework(ctx context.Context, input types.DeleteFrameworkInput) (*types.DeleteFrameworkPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.FrameworkID.TenantID())
err := svc.Frameworks.Delete(ctx, input.FrameworkID)
if err != nil {
return nil, fmt.Errorf("cannot delete framework: %w", err)
}
return &types.DeleteFrameworkPayload{
DeletedFrameworkID: input.FrameworkID,
}, nil
}
// ImportFramework is the resolver for the importFramework field.
func (r *mutationResolver) ImportFramework(ctx context.Context, input types.ImportFrameworkInput) (*types.ImportFrameworkPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
req := probo.ImportFrameworkRequest{}
if err := json.NewDecoder(input.File.File).Decode(&req.Data); err != nil {
if err := json.NewDecoder(input.File.File).Decode(&req.Framework); err != nil {
return nil, fmt.Errorf("cannot decode framework: %w", err)
}
@@ -395,16 +405,16 @@ func (r *mutationResolver) ImportFramework(ctx context.Context, input types.Impo
}, nil
}
// CreateMitigation is the resolver for the createMitigation field.
// // CreateMitigation is the resolver for the createMitigation field.
func (r *mutationResolver) CreateMitigation(ctx context.Context, input types.CreateMitigationInput) (*types.CreateMitigationPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.FrameworkID.TenantID())
svc := r.GetTenantServiceIfAuthorized(ctx, input.OrganizationID.TenantID())
mitigation, err := svc.Mitigations.Create(ctx, probo.CreateMitigationRequest{
FrameworkID: input.FrameworkID,
Name: input.Name,
Description: input.Description,
Category: input.Category,
Importance: input.Importance,
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: input.Description,
Category: input.Category,
Importance: input.Importance,
})
if err != nil {
panic(fmt.Errorf("cannot create mitigation: %w", err))
@@ -420,13 +430,12 @@ func (r *mutationResolver) UpdateMitigation(ctx context.Context, input types.Upd
svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID())
mitigation, err := svc.Mitigations.Update(ctx, probo.UpdateMitigationRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
Name: input.Name,
Description: input.Description,
Category: input.Category,
Importance: input.Importance,
State: input.State,
ID: input.ID,
Name: input.Name,
Description: input.Description,
Category: input.Category,
Importance: input.Importance,
State: input.State,
})
if err != nil {
panic(fmt.Errorf("cannot update mitigation: %w", err))
@@ -515,13 +524,12 @@ func (r *mutationResolver) UpdatePolicy(ctx context.Context, input types.UpdateP
svc := r.GetTenantServiceIfAuthorized(ctx, input.ID.TenantID())
policy, err := svc.Policies.Update(ctx, probo.UpdatePolicyRequest{
ID: input.ID,
ExpectedVersion: input.ExpectedVersion,
Name: input.Name,
Content: input.Content,
Status: input.Status,
ReviewDate: input.ReviewDate,
OwnerID: input.OwnerID,
ID: input.ID,
Name: input.Name,
Content: input.Content,
Status: input.Status,
ReviewDate: input.ReviewDate,
OwnerID: input.OwnerID,
})
if err != nil {
return nil, fmt.Errorf("cannot update policy: %w", err)
@@ -733,6 +741,31 @@ func (r *organizationResolver) Policies(ctx context.Context, obj *types.Organiza
return types.NewPolicyConnection(page), nil
}
// Mitigations is the resolver for the mitigations field.
func (r *organizationResolver) Mitigations(ctx context.Context, obj *types.Organization, 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.ListForOrganizationID(ctx, obj.ID, cursor)
if err != nil {
return nil, fmt.Errorf("cannot list organization mitigations: %w", err)
}
return types.NewMitigationConnection(page), nil
}
// Owner is the resolver for the owner field.
func (r *policyResolver) Owner(ctx context.Context, obj *types.Policy) (*types.People, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())