Change state of applicability
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -38,6 +38,7 @@ type (
|
||||
Description *string `db:"description"`
|
||||
Status ControlStatus `db:"status"`
|
||||
ExclusionJustification *string `db:"exclusion_justification"`
|
||||
BestPractice bool `db:"best_practice"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
@@ -134,6 +135,7 @@ WITH ctrl AS (
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
@@ -153,6 +155,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -245,6 +248,7 @@ WITH ctrl AS (
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
@@ -264,6 +268,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -362,6 +367,7 @@ WITH ctrl AS (
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
@@ -387,6 +393,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -467,6 +474,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -562,6 +570,7 @@ WITH ctrl AS (
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
@@ -581,6 +590,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -628,6 +638,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -677,6 +688,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -726,6 +738,7 @@ INSERT INTO
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
@@ -739,6 +752,7 @@ VALUES (
|
||||
@description,
|
||||
@status,
|
||||
@exclusion_justification,
|
||||
@best_practice,
|
||||
@created_at,
|
||||
@updated_at
|
||||
);
|
||||
@@ -754,6 +768,7 @@ VALUES (
|
||||
"description": c.Description,
|
||||
"status": c.Status,
|
||||
"exclusion_justification": c.ExclusionJustification,
|
||||
"best_practice": c.BestPractice,
|
||||
"created_at": c.CreatedAt,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}
|
||||
@@ -808,6 +823,7 @@ UPDATE controls SET
|
||||
section_title = @section_title,
|
||||
status = @status,
|
||||
exclusion_justification = @exclusion_justification,
|
||||
best_practice = @best_practice,
|
||||
updated_at = @updated_at
|
||||
WHERE %s
|
||||
AND id = @control_id
|
||||
@@ -821,6 +837,7 @@ WHERE %s
|
||||
"section_title": c.SectionTitle,
|
||||
"status": c.Status,
|
||||
"exclusion_justification": c.ExclusionJustification,
|
||||
"best_practice": c.BestPractice,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -905,6 +922,7 @@ WITH ctrl AS (
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
@@ -924,6 +942,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -1017,6 +1036,7 @@ WITH ctrl AS (
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
@@ -1036,6 +1056,7 @@ SELECT
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -1065,3 +1086,117 @@ WHERE %s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controls) CountByStateOfApplicabilityID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
stateOfApplicabilityID gid.GID,
|
||||
filter *ControlFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH ctrl AS (
|
||||
SELECT
|
||||
c.id,
|
||||
c.tenant_id,
|
||||
c.search_vector
|
||||
FROM
|
||||
controls c
|
||||
INNER JOIN
|
||||
states_of_applicability_controls soac ON c.id = soac.control_id
|
||||
WHERE
|
||||
soac.state_of_applicability_id = @state_of_applicability_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
ctrl
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"state_of_applicability_id": stateOfApplicabilityID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (c *Controls) LoadByStateOfApplicabilityID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
stateOfApplicabilityID gid.GID,
|
||||
cursor *page.Cursor[ControlOrderField],
|
||||
filter *ControlFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH ctrl AS (
|
||||
SELECT
|
||||
c.id,
|
||||
c.section_title,
|
||||
c.framework_id,
|
||||
c.organization_id,
|
||||
c.tenant_id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.status,
|
||||
c.exclusion_justification,
|
||||
c.best_practice,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
c.search_vector
|
||||
FROM
|
||||
controls c
|
||||
INNER JOIN
|
||||
states_of_applicability_controls soac ON c.id = soac.control_id
|
||||
WHERE
|
||||
soac.state_of_applicability_id = @state_of_applicability_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
section_title,
|
||||
framework_id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
status,
|
||||
exclusion_justification,
|
||||
best_practice,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
ctrl
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"state_of_applicability_id": stateOfApplicabilityID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.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
|
||||
}
|
||||
|
||||
@@ -136,3 +136,56 @@ WHERE
|
||||
*cms = controlMeasures
|
||||
return nil
|
||||
}
|
||||
|
||||
type ControlWithRisk struct {
|
||||
ControlID gid.GID `db:"control_id"`
|
||||
}
|
||||
|
||||
type ControlsWithRisk []*ControlWithRisk
|
||||
|
||||
func (cwrs *ControlsWithRisk) LoadByControlIDs(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlIDs []gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
WITH control_risks AS (
|
||||
SELECT DISTINCT
|
||||
cm.control_id,
|
||||
rm.risk_id,
|
||||
r.tenant_id
|
||||
FROM
|
||||
controls_measures cm
|
||||
INNER JOIN
|
||||
risks_measures rm ON cm.measure_id = rm.measure_id
|
||||
INNER JOIN
|
||||
risks r ON rm.risk_id = r.id
|
||||
WHERE
|
||||
cm.control_id = ANY(@control_ids)
|
||||
)
|
||||
SELECT DISTINCT
|
||||
control_id
|
||||
FROM
|
||||
control_risks
|
||||
WHERE
|
||||
%s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"control_ids": controlIDs}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query control risks: %w", err)
|
||||
}
|
||||
|
||||
controlsWithRisk, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlWithRisk])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect control risks: %w", err)
|
||||
}
|
||||
|
||||
*cwrs = controlsWithRisk
|
||||
return nil
|
||||
}
|
||||
|
||||
221
pkg/coredata/control_obligation.go
Normal file
221
pkg/coredata/control_obligation.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// 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/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
ControlObligation struct {
|
||||
ControlID gid.GID `db:"control_id"`
|
||||
ObligationID gid.GID `db:"obligation_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
ControlObligations []*ControlObligation
|
||||
)
|
||||
|
||||
func (co ControlObligation) Upsert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
controls_obligations (
|
||||
control_id,
|
||||
obligation_id,
|
||||
tenant_id,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
@control_id,
|
||||
@obligation_id,
|
||||
@tenant_id,
|
||||
@created_at
|
||||
)
|
||||
ON CONFLICT (control_id, obligation_id) DO NOTHING;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"control_id": co.ControlID,
|
||||
"obligation_id": co.ObligationID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"created_at": co.CreatedAt,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (co ControlObligation) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
obligationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
DELETE
|
||||
FROM
|
||||
controls_obligations
|
||||
WHERE
|
||||
%s
|
||||
AND control_id = @control_id
|
||||
AND obligation_id = @obligation_id;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"control_id": controlID,
|
||||
"obligation_id": obligationID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (cos *ControlObligations) LoadByControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
control_id,
|
||||
obligation_id,
|
||||
created_at
|
||||
FROM
|
||||
controls_obligations
|
||||
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_obligations: %w", err)
|
||||
}
|
||||
|
||||
controlObligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlObligation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect control_obligations: %w", err)
|
||||
}
|
||||
|
||||
*cos = controlObligations
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cos *ControlObligations) LoadByObligationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
obligationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
control_id,
|
||||
obligation_id,
|
||||
created_at
|
||||
FROM
|
||||
controls_obligations
|
||||
WHERE
|
||||
%s
|
||||
AND obligation_id = @obligation_id
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"obligation_id": obligationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query control_obligations: %w", err)
|
||||
}
|
||||
|
||||
controlObligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlObligation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect control_obligations: %w", err)
|
||||
}
|
||||
|
||||
*cos = controlObligations
|
||||
return nil
|
||||
}
|
||||
|
||||
type ControlObligationType struct {
|
||||
ControlID gid.GID `db:"control_id"`
|
||||
Type ObligationType `db:"type"`
|
||||
}
|
||||
|
||||
type ControlObligationTypes []*ControlObligationType
|
||||
|
||||
func (cots *ControlObligationTypes) LoadTypesByControlIDs(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlIDs []gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
WITH control_obls AS (
|
||||
SELECT
|
||||
co.control_id,
|
||||
o.type,
|
||||
o.tenant_id
|
||||
FROM
|
||||
controls_obligations co
|
||||
INNER JOIN
|
||||
obligations o ON co.obligation_id = o.id
|
||||
WHERE
|
||||
co.control_id = ANY(@control_ids)
|
||||
)
|
||||
SELECT DISTINCT
|
||||
control_id,
|
||||
type
|
||||
FROM
|
||||
control_obls
|
||||
WHERE
|
||||
%s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"control_ids": controlIDs}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query control obligations: %w", err)
|
||||
}
|
||||
|
||||
controlObligationTypes, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlObligationType])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect control obligations: %w", err)
|
||||
}
|
||||
|
||||
*cots = controlObligationTypes
|
||||
return nil
|
||||
}
|
||||
@@ -70,6 +70,8 @@ const (
|
||||
DataProtectionImpactAssessmentEntityType uint16 = 46
|
||||
TransferImpactAssessmentEntityType uint16 = 47
|
||||
RightsRequestEntityType uint16 = 48
|
||||
StateOfApplicabilityEntityType uint16 = 49
|
||||
StateOfApplicabilityControlEntityType uint16 = 50
|
||||
)
|
||||
|
||||
type EntityInfo struct {
|
||||
@@ -274,6 +276,14 @@ var entityRegistry = map[uint16]EntityInfo{
|
||||
Model: "RightsRequest",
|
||||
Table: "rights_requests",
|
||||
},
|
||||
StateOfApplicabilityEntityType: {
|
||||
Model: "StateOfApplicability",
|
||||
Table: "states_of_applicability",
|
||||
},
|
||||
StateOfApplicabilityControlEntityType: {
|
||||
Model: "StateOfApplicabilityControl",
|
||||
Table: "states_of_applicability_controls",
|
||||
},
|
||||
}
|
||||
|
||||
func EntityTable(entityType uint16) (string, bool) {
|
||||
|
||||
161
pkg/coredata/migrations/20260102T134633Z.sql
Normal file
161
pkg/coredata/migrations/20260102T134633Z.sql
Normal file
@@ -0,0 +1,161 @@
|
||||
CREATE TABLE states_of_applicability (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
organization_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
source_id TEXT,
|
||||
snapshot_id TEXT,
|
||||
owner_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
|
||||
CONSTRAINT states_of_applicability_organization_id_fkey
|
||||
FOREIGN KEY (organization_id)
|
||||
REFERENCES organizations(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT states_of_applicability_snapshot_id_fkey
|
||||
FOREIGN KEY (snapshot_id)
|
||||
REFERENCES snapshots(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
|
||||
CONSTRAINT states_of_applicability_owner_id_fkey
|
||||
FOREIGN KEY (owner_id)
|
||||
REFERENCES peoples(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX states_of_applicability_source_id_snapshot_id_uniq
|
||||
ON states_of_applicability (source_id, snapshot_id)
|
||||
WHERE snapshot_id IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX states_of_applicability_name_organization_id_uniq
|
||||
ON states_of_applicability (name, organization_id)
|
||||
WHERE snapshot_id IS NULL;
|
||||
|
||||
CREATE TABLE states_of_applicability_controls (
|
||||
id TEXT PRIMARY KEY,
|
||||
state_of_applicability_id TEXT NOT NULL REFERENCES states_of_applicability(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
control_id TEXT NOT NULL REFERENCES controls(id) ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
organization_id TEXT NOT NULL,
|
||||
tenant_id TEXT NOT NULL,
|
||||
snapshot_id TEXT,
|
||||
applicability BOOLEAN NOT NULL,
|
||||
justification TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
|
||||
CONSTRAINT states_of_applicability_controls_organization_id_fkey
|
||||
FOREIGN KEY (organization_id)
|
||||
REFERENCES organizations(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT states_of_applicability_controls_snapshot_id_fkey
|
||||
FOREIGN KEY (snapshot_id)
|
||||
REFERENCES snapshots(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
|
||||
UNIQUE (state_of_applicability_id, control_id)
|
||||
);
|
||||
|
||||
ALTER TABLE controls ADD COLUMN best_practice BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
ALTER TABLE controls ALTER COLUMN best_practice DROP DEFAULT;
|
||||
|
||||
ALTER TYPE snapshots_type ADD VALUE 'STATES_OF_APPLICABILITY';
|
||||
|
||||
CREATE TYPE obligation_type AS ENUM (
|
||||
'LEGAL',
|
||||
'CONTRACTUAL'
|
||||
);
|
||||
|
||||
ALTER TABLE obligations ADD COLUMN type obligation_type NOT NULL DEFAULT 'LEGAL';
|
||||
ALTER TABLE obligations ALTER COLUMN type DROP DEFAULT;
|
||||
|
||||
CREATE TABLE controls_obligations (
|
||||
control_id TEXT NOT NULL REFERENCES controls(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
obligation_id TEXT NOT NULL REFERENCES obligations(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
tenant_id TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY (control_id, obligation_id)
|
||||
);
|
||||
|
||||
INSERT INTO states_of_applicability (
|
||||
id,
|
||||
tenant_id,
|
||||
organization_id,
|
||||
name,
|
||||
source_id,
|
||||
snapshot_id,
|
||||
owner_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(f.tenant_id), 49) as id,
|
||||
f.tenant_id,
|
||||
f.organization_id,
|
||||
f.name,
|
||||
NULL as source_id,
|
||||
NULL as snapshot_id,
|
||||
(SELECT id FROM peoples WHERE tenant_id = f.tenant_id LIMIT 1) as owner_id,
|
||||
NOW() as created_at,
|
||||
NOW() as updated_at
|
||||
FROM frameworks f
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM states_of_applicability soa
|
||||
WHERE soa.name = f.name
|
||||
AND soa.snapshot_id IS NULL
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM peoples WHERE tenant_id = f.tenant_id
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM controls c
|
||||
WHERE c.framework_id = f.id
|
||||
AND (
|
||||
c.status = 'EXCLUDED'
|
||||
OR (c.exclusion_justification IS NOT NULL AND c.exclusion_justification != '')
|
||||
)
|
||||
);
|
||||
|
||||
INSERT INTO states_of_applicability_controls (
|
||||
id,
|
||||
state_of_applicability_id,
|
||||
control_id,
|
||||
organization_id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
applicability,
|
||||
justification,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(c.tenant_id), 50) as id,
|
||||
soa.id as state_of_applicability_id,
|
||||
c.id as control_id,
|
||||
c.organization_id,
|
||||
c.tenant_id,
|
||||
NULL as snapshot_id,
|
||||
CASE
|
||||
WHEN c.status = 'EXCLUDED' THEN FALSE
|
||||
ELSE TRUE
|
||||
END as applicability,
|
||||
c.exclusion_justification,
|
||||
NOW() as created_at,
|
||||
NOW() as updated_at
|
||||
FROM frameworks f
|
||||
JOIN states_of_applicability soa ON soa.name = f.name AND soa.snapshot_id IS NULL
|
||||
JOIN controls c ON c.framework_id = f.id
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM states_of_applicability_controls soac
|
||||
WHERE soac.state_of_applicability_id = soa.id
|
||||
AND soac.control_id = c.id
|
||||
);
|
||||
@@ -20,10 +20,10 @@ import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -39,6 +39,7 @@ type (
|
||||
LastReviewDate *time.Time `db:"last_review_date"`
|
||||
DueDate *time.Time `db:"due_date"`
|
||||
Status ObligationStatus `db:"status"`
|
||||
Type ObligationType `db:"type"`
|
||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
||||
SourceID *gid.GID `db:"source_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
@@ -84,6 +85,7 @@ SELECT
|
||||
last_review_date,
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -216,6 +218,7 @@ SELECT
|
||||
last_review_date,
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
created_at,
|
||||
@@ -273,6 +276,7 @@ WITH obls AS (
|
||||
o.last_review_date,
|
||||
o.due_date,
|
||||
o.status,
|
||||
o.type,
|
||||
o.snapshot_id,
|
||||
o.source_id,
|
||||
o.created_at,
|
||||
@@ -298,6 +302,7 @@ SELECT
|
||||
last_review_date,
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
created_at,
|
||||
@@ -331,6 +336,131 @@ WHERE %s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (os *Obligations) CountByControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
SELECT
|
||||
o.id,
|
||||
o.tenant_id,
|
||||
o.snapshot_id
|
||||
FROM
|
||||
obligations o
|
||||
INNER JOIN
|
||||
controls_obligations co ON o.id = co.obligation_id
|
||||
WHERE
|
||||
co.control_id = @control_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
err := row.Scan(&count)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot count obligations: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (os *Obligations) LoadByControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
SELECT
|
||||
o.id,
|
||||
o.organization_id,
|
||||
o.area,
|
||||
o.source,
|
||||
o.requirement,
|
||||
o.actions_to_be_implemented,
|
||||
o.regulator,
|
||||
o.owner_id,
|
||||
o.last_review_date,
|
||||
o.due_date,
|
||||
o.status,
|
||||
o.type,
|
||||
o.snapshot_id,
|
||||
o.source_id,
|
||||
o.created_at,
|
||||
o.updated_at,
|
||||
o.tenant_id
|
||||
FROM
|
||||
obligations o
|
||||
INNER JOIN
|
||||
controls_obligations co ON o.id = co.obligation_id
|
||||
WHERE
|
||||
co.control_id = @control_id
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
area,
|
||||
source,
|
||||
requirement,
|
||||
actions_to_be_implemented,
|
||||
regulator,
|
||||
owner_id,
|
||||
last_review_date,
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query obligations: %w", err)
|
||||
}
|
||||
|
||||
obligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Obligation])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect obligations: %w", err)
|
||||
}
|
||||
|
||||
*os = obligations
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Obligation) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -350,6 +480,7 @@ INSERT INTO obligations (
|
||||
last_review_date,
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
created_at,
|
||||
@@ -367,6 +498,7 @@ INSERT INTO obligations (
|
||||
@last_review_date,
|
||||
@due_date,
|
||||
@status,
|
||||
@type,
|
||||
@snapshot_id,
|
||||
@source_id,
|
||||
@created_at,
|
||||
@@ -387,6 +519,7 @@ INSERT INTO obligations (
|
||||
"last_review_date": o.LastReviewDate,
|
||||
"due_date": o.DueDate,
|
||||
"status": o.Status,
|
||||
"type": o.Type,
|
||||
"snapshot_id": o.SnapshotID,
|
||||
"source_id": o.SourceID,
|
||||
"created_at": o.CreatedAt,
|
||||
@@ -417,6 +550,7 @@ UPDATE obligations SET
|
||||
last_review_date = @last_review_date,
|
||||
due_date = @due_date,
|
||||
status = @status,
|
||||
type = @type,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
%s
|
||||
@@ -437,6 +571,7 @@ WHERE
|
||||
"last_review_date": o.LastReviewDate,
|
||||
"due_date": o.DueDate,
|
||||
"status": o.Status,
|
||||
"type": o.Type,
|
||||
"updated_at": o.UpdatedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
@@ -492,6 +627,7 @@ INSERT INTO obligations (
|
||||
last_review_date,
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
@@ -510,6 +646,7 @@ SELECT
|
||||
o.last_review_date,
|
||||
o.due_date,
|
||||
o.status,
|
||||
o.type,
|
||||
o.created_at,
|
||||
o.updated_at
|
||||
FROM obligations o
|
||||
|
||||
64
pkg/coredata/obligation_type.go
Normal file
64
pkg/coredata/obligation_type.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ObligationType string
|
||||
|
||||
const (
|
||||
ObligationTypeLegal ObligationType = "LEGAL"
|
||||
ObligationTypeContractual ObligationType = "CONTRACTUAL"
|
||||
)
|
||||
|
||||
func ObligationTypes() []ObligationType {
|
||||
return []ObligationType{
|
||||
ObligationTypeLegal,
|
||||
ObligationTypeContractual,
|
||||
}
|
||||
}
|
||||
|
||||
func (ot ObligationType) String() string {
|
||||
return string(ot)
|
||||
}
|
||||
|
||||
func (ot *ObligationType) Scan(value any) error {
|
||||
var s string
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
s = v
|
||||
case []byte:
|
||||
s = string(v)
|
||||
default:
|
||||
return fmt.Errorf("unsupported type for ObligationType: %T", value)
|
||||
}
|
||||
|
||||
switch s {
|
||||
case "LEGAL":
|
||||
*ot = ObligationTypeLegal
|
||||
case "CONTRACTUAL":
|
||||
*ot = ObligationTypeContractual
|
||||
default:
|
||||
return fmt.Errorf("invalid ObligationType value: %q", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ot ObligationType) Value() (driver.Value, error) {
|
||||
return ot.String(), nil
|
||||
}
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
SnapshotsTypeObligations SnapshotsType = "OBLIGATIONS"
|
||||
SnapshotsTypeContinualImprovements SnapshotsType = "CONTINUAL_IMPROVEMENTS"
|
||||
SnapshotsTypeProcessingActivities SnapshotsType = "PROCESSING_ACTIVITIES"
|
||||
SnapshotsTypeStatesOfApplicability SnapshotsType = "STATES_OF_APPLICABILITY"
|
||||
)
|
||||
|
||||
func SnapshotsTypes() []SnapshotsType {
|
||||
@@ -41,6 +42,10 @@ func SnapshotsTypes() []SnapshotsType {
|
||||
SnapshotsTypeAssets,
|
||||
SnapshotsTypeData,
|
||||
SnapshotsTypeNonconformities,
|
||||
SnapshotsTypeObligations,
|
||||
SnapshotsTypeContinualImprovements,
|
||||
SnapshotsTypeProcessingActivities,
|
||||
SnapshotsTypeStatesOfApplicability,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +81,8 @@ func (st *SnapshotsType) Scan(value any) error {
|
||||
*st = SnapshotsTypeContinualImprovements
|
||||
case SnapshotsTypeProcessingActivities.String():
|
||||
*st = SnapshotsTypeProcessingActivities
|
||||
case SnapshotsTypeStatesOfApplicability.String():
|
||||
*st = SnapshotsTypeStatesOfApplicability
|
||||
default:
|
||||
return fmt.Errorf("invalid SnapshotsType value: %q", s)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type Snapshottable interface {
|
||||
@@ -44,6 +44,8 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
|
||||
return ProcessingActivities{}, nil
|
||||
case SnapshotsTypeVendors:
|
||||
return Vendors{}, nil
|
||||
case SnapshotsTypeStatesOfApplicability:
|
||||
return StatesOfApplicability{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported snapshot type: %s", snapshotType)
|
||||
}
|
||||
|
||||
466
pkg/coredata/state_of_applicability.go
Normal file
466
pkg/coredata/state_of_applicability.go
Normal file
@@ -0,0 +1,466 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
StateOfApplicability struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Name string `db:"name"`
|
||||
SourceID *gid.GID `db:"source_id"`
|
||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
||||
OwnerID gid.GID `db:"owner_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
StatesOfApplicability []*StateOfApplicability
|
||||
|
||||
ErrStateOfApplicabilityNotFound struct {
|
||||
Identifier string
|
||||
}
|
||||
|
||||
ErrStateOfApplicabilityAlreadyExists struct {
|
||||
message string
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrStateOfApplicabilityNotFound) Error() string {
|
||||
return fmt.Sprintf("state of applicability not found: %s", e.Identifier)
|
||||
}
|
||||
|
||||
func (e ErrStateOfApplicabilityAlreadyExists) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (s StateOfApplicability) CursorKey(orderBy StateOfApplicabilityOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case StateOfApplicabilityOrderFieldCreatedAt:
|
||||
return page.NewCursorKey(s.ID, s.CreatedAt)
|
||||
case StateOfApplicabilityOrderFieldName:
|
||||
return page.NewCursorKey(s.ID, s.Name)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (s *StateOfApplicability) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
stateOfApplicabilityID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
source_id,
|
||||
snapshot_id,
|
||||
owner_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
states_of_applicability
|
||||
WHERE
|
||||
%s
|
||||
AND id = @state_of_applicability_id
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"state_of_applicability_id": stateOfApplicabilityID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query states_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
stateOfApplicability, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[StateOfApplicability])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrStateOfApplicabilityNotFound{Identifier: stateOfApplicabilityID.String()}
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot collect state_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
*s = stateOfApplicability
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StatesOfApplicability) LoadByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[StateOfApplicabilityOrderField],
|
||||
filter *StateOfApplicabilityFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
source_id,
|
||||
snapshot_id,
|
||||
owner_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
states_of_applicability
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query states_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
statesOfApplicability, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[StateOfApplicability])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect states_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
*s = statesOfApplicability
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StatesOfApplicability) CountByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
filter *StateOfApplicabilityFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM
|
||||
states_of_applicability
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot count states_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (s *StateOfApplicability) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
states_of_applicability (
|
||||
tenant_id,
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
source_id,
|
||||
snapshot_id,
|
||||
owner_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
@tenant_id,
|
||||
@state_of_applicability_id,
|
||||
@organization_id,
|
||||
@name,
|
||||
@source_id,
|
||||
@snapshot_id,
|
||||
@owner_id,
|
||||
@created_at,
|
||||
@updated_at
|
||||
);
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"state_of_applicability_id": s.ID,
|
||||
"organization_id": s.OrganizationID,
|
||||
"name": s.Name,
|
||||
"source_id": s.SourceID,
|
||||
"snapshot_id": s.SnapshotID,
|
||||
"owner_id": s.OwnerID,
|
||||
"created_at": s.CreatedAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
return &ErrStateOfApplicabilityAlreadyExists{
|
||||
message: fmt.Sprintf("state of applicability with name %q already exists", s.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot insert state_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StateOfApplicability) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE states_of_applicability
|
||||
SET
|
||||
name = @name,
|
||||
owner_id = @owner_id,
|
||||
updated_at = @updated_at
|
||||
WHERE %s
|
||||
AND id = @state_of_applicability_id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": s.ID,
|
||||
"name": s.Name,
|
||||
"owner_id": s.OwnerID,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
result, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
return &ErrStateOfApplicabilityAlreadyExists{
|
||||
message: fmt.Sprintf("state of applicability with name %q already exists", s.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot update state_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrStateOfApplicabilityNotFound{Identifier: s.ID.String()}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StateOfApplicability) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
DELETE FROM states_of_applicability
|
||||
WHERE %s
|
||||
AND id = @state_of_applicability_id
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": s.ID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
result, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot delete state_of_applicability: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return &ErrStateOfApplicabilityNotFound{Identifier: s.ID.String()}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (soas StatesOfApplicability) Snapshot(ctx context.Context, conn pg.Conn, scope Scoper, organizationID, snapshotID gid.GID) error {
|
||||
if err := soas.insertStateOfApplicabilitySnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil {
|
||||
return fmt.Errorf("cannot insert state_of_applicability snapshots: %w", err)
|
||||
}
|
||||
|
||||
if err := soas.insertStateOfApplicabilityControlSnapshots(ctx, conn, scope, organizationID, snapshotID); err != nil {
|
||||
return fmt.Errorf("cannot insert state_of_applicability_control snapshots: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (soas StatesOfApplicability) insertStateOfApplicabilitySnapshots(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
snapshotID gid.GID,
|
||||
) error {
|
||||
query := `
|
||||
INSERT INTO states_of_applicability (
|
||||
id,
|
||||
tenant_id,
|
||||
organization_id,
|
||||
name,
|
||||
source_id,
|
||||
snapshot_id,
|
||||
owner_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(@tenant_id), @state_of_applicability_entity_type),
|
||||
@tenant_id,
|
||||
soa.organization_id,
|
||||
soa.name,
|
||||
soa.id,
|
||||
@snapshot_id,
|
||||
soa.owner_id,
|
||||
soa.created_at,
|
||||
soa.updated_at
|
||||
FROM states_of_applicability soa
|
||||
WHERE %s
|
||||
AND soa.organization_id = @organization_id
|
||||
AND soa.snapshot_id IS NULL
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
"state_of_applicability_entity_type": StateOfApplicabilityEntityType,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert state_of_applicability snapshots: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (soas StatesOfApplicability) insertStateOfApplicabilityControlSnapshots(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
snapshotID gid.GID,
|
||||
) error {
|
||||
query := `
|
||||
WITH source_soa AS (
|
||||
SELECT id, organization_id
|
||||
FROM states_of_applicability
|
||||
WHERE %s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
),
|
||||
snapshot_soa AS (
|
||||
SELECT id, source_id
|
||||
FROM states_of_applicability
|
||||
WHERE snapshot_id = @snapshot_id
|
||||
)
|
||||
INSERT INTO states_of_applicability_controls (
|
||||
id,
|
||||
state_of_applicability_id,
|
||||
control_id,
|
||||
organization_id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
applicability,
|
||||
justification,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(@tenant_id), @state_of_applicability_control_entity_type),
|
||||
snapshot_soa.id,
|
||||
soac.control_id,
|
||||
soac.organization_id,
|
||||
@tenant_id,
|
||||
@snapshot_id,
|
||||
soac.applicability,
|
||||
soac.justification,
|
||||
soac.created_at,
|
||||
soac.updated_at
|
||||
FROM states_of_applicability_controls soac
|
||||
INNER JOIN source_soa
|
||||
ON soac.state_of_applicability_id = source_soa.id
|
||||
INNER JOIN snapshot_soa
|
||||
ON snapshot_soa.source_id = source_soa.id
|
||||
WHERE soac.snapshot_id IS NULL
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
"state_of_applicability_control_entity_type": StateOfApplicabilityControlEntityType,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert state_of_applicability_control snapshots: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
607
pkg/coredata/state_of_applicability_control.go
Normal file
607
pkg/coredata/state_of_applicability_control.go
Normal file
@@ -0,0 +1,607 @@
|
||||
// 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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
StateOfApplicabilityControl struct {
|
||||
ID gid.GID `db:"id"`
|
||||
StateOfApplicabilityID gid.GID `db:"state_of_applicability_id"`
|
||||
ControlID gid.GID `db:"control_id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
SnapshotID *gid.GID `db:"snapshot_id"`
|
||||
Applicability bool `db:"applicability"`
|
||||
Justification *string `db:"justification"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
StateOfApplicabilityControls []*StateOfApplicabilityControl
|
||||
|
||||
AvailableStateOfApplicabilityControl struct {
|
||||
ControlID gid.GID `db:"control_id"`
|
||||
SectionTitle string `db:"section_title"`
|
||||
Name string `db:"name"`
|
||||
FrameworkID gid.GID `db:"framework_id"`
|
||||
FrameworkName string `db:"framework_name"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
StateOfApplicabilityID *gid.GID `db:"state_of_applicability_id"`
|
||||
Applicability *bool `db:"applicability"`
|
||||
Justification *string `db:"justification"`
|
||||
BestPractice bool `db:"best_practice"`
|
||||
Regulatory bool `db:"regulatory"`
|
||||
Contractual bool `db:"contractual"`
|
||||
RiskAssessment bool `db:"risk_assessment"`
|
||||
}
|
||||
|
||||
AvailableStateOfApplicabilityControls []*AvailableStateOfApplicabilityControl
|
||||
|
||||
ErrStateOfApplicabilityControlNotFound struct {
|
||||
StateOfApplicabilityID gid.GID
|
||||
ControlID gid.GID
|
||||
}
|
||||
|
||||
ErrStateOfApplicabilityControlAlreadyExists struct {
|
||||
StateOfApplicabilityID gid.GID
|
||||
ControlID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrStateOfApplicabilityControlNotFound) Error() string {
|
||||
return fmt.Sprintf("state of applicability control not found: state_of_applicability_id=%s, control_id=%s", e.StateOfApplicabilityID, e.ControlID)
|
||||
}
|
||||
|
||||
func (e ErrStateOfApplicabilityControlAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("state of applicability control already exists: state_of_applicability_id=%s, control_id=%s", e.StateOfApplicabilityID, e.ControlID)
|
||||
}
|
||||
|
||||
func (s StateOfApplicabilityControl) CursorKey(orderBy StateOfApplicabilityOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case StateOfApplicabilityOrderFieldName:
|
||||
return page.NewCursorKey(s.ID, s.StateOfApplicabilityID)
|
||||
case StateOfApplicabilityOrderFieldCreatedAt:
|
||||
return page.NewCursorKey(s.ID, s.CreatedAt)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||
}
|
||||
|
||||
func (sac *StateOfApplicabilityControl) LoadByStateOfApplicabilityIDAndControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
stateOfApplicabilityID gid.GID,
|
||||
controlID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
WITH current_soa AS (
|
||||
SELECT id
|
||||
FROM states_of_applicability
|
||||
WHERE %s
|
||||
AND id = @state_of_applicability_id
|
||||
AND snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
soac.id,
|
||||
soac.state_of_applicability_id,
|
||||
soac.control_id,
|
||||
soac.organization_id,
|
||||
soac.snapshot_id,
|
||||
soac.applicability,
|
||||
soac.justification,
|
||||
soac.created_at,
|
||||
soac.updated_at
|
||||
FROM
|
||||
states_of_applicability_controls soac
|
||||
INNER JOIN
|
||||
current_soa ON soac.state_of_applicability_id = current_soa.id
|
||||
WHERE
|
||||
soac.control_id = @control_id
|
||||
LIMIT 1;
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": stateOfApplicabilityID,
|
||||
"control_id": controlID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query states_of_applicability_controls: %w", err)
|
||||
}
|
||||
|
||||
control, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[StateOfApplicabilityControl])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &ErrStateOfApplicabilityControlNotFound{
|
||||
StateOfApplicabilityID: stateOfApplicabilityID,
|
||||
ControlID: controlID,
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("cannot collect state of applicability control: %w", err)
|
||||
}
|
||||
|
||||
*sac = control
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sac *StateOfApplicabilityControl) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
states_of_applicability_controls (
|
||||
id,
|
||||
state_of_applicability_id,
|
||||
control_id,
|
||||
organization_id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
applicability,
|
||||
justification,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
@id,
|
||||
@state_of_applicability_id,
|
||||
@control_id,
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@snapshot_id,
|
||||
@applicability,
|
||||
@justification,
|
||||
@created_at,
|
||||
@updated_at
|
||||
);
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": sac.ID,
|
||||
"state_of_applicability_id": sac.StateOfApplicabilityID,
|
||||
"control_id": sac.ControlID,
|
||||
"organization_id": sac.OrganizationID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": sac.SnapshotID,
|
||||
"applicability": sac.Applicability,
|
||||
"justification": sac.Justification,
|
||||
"created_at": sac.CreatedAt,
|
||||
"updated_at": sac.UpdatedAt,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) {
|
||||
if pgErr.Code == "23505" {
|
||||
return &ErrStateOfApplicabilityControlAlreadyExists{
|
||||
StateOfApplicabilityID: sac.StateOfApplicabilityID,
|
||||
ControlID: sac.ControlID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot insert state_of_applicability_control: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sac *StateOfApplicabilityControl) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE states_of_applicability_controls
|
||||
SET
|
||||
applicability = @applicability,
|
||||
justification = @justification,
|
||||
updated_at = @updated_at
|
||||
WHERE %s
|
||||
AND state_of_applicability_id = @state_of_applicability_id
|
||||
AND control_id = @control_id
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": sac.StateOfApplicabilityID,
|
||||
"control_id": sac.ControlID,
|
||||
"applicability": sac.Applicability,
|
||||
"justification": sac.Justification,
|
||||
"updated_at": sac.UpdatedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update state_of_applicability_control: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sac *StateOfApplicabilityControl) Upsert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO
|
||||
states_of_applicability_controls (
|
||||
id,
|
||||
state_of_applicability_id,
|
||||
control_id,
|
||||
organization_id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
applicability,
|
||||
justification,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
@id,
|
||||
@state_of_applicability_id,
|
||||
@control_id,
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@snapshot_id,
|
||||
@applicability,
|
||||
@justification,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
ON CONFLICT (state_of_applicability_id, control_id) DO UPDATE SET
|
||||
applicability = EXCLUDED.applicability,
|
||||
justification = EXCLUDED.justification,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": sac.ID,
|
||||
"state_of_applicability_id": sac.StateOfApplicabilityID,
|
||||
"control_id": sac.ControlID,
|
||||
"organization_id": sac.OrganizationID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": sac.SnapshotID,
|
||||
"applicability": sac.Applicability,
|
||||
"justification": sac.Justification,
|
||||
"created_at": sac.CreatedAt,
|
||||
"updated_at": sac.UpdatedAt,
|
||||
}
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot upsert state_of_applicability_control: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sac *StateOfApplicabilityControl) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
WITH current_soa AS (
|
||||
SELECT id
|
||||
FROM states_of_applicability
|
||||
WHERE %s
|
||||
AND id = @state_of_applicability_id
|
||||
AND snapshot_id IS NULL
|
||||
)
|
||||
DELETE FROM states_of_applicability_controls
|
||||
WHERE state_of_applicability_id IN (SELECT id FROM current_soa)
|
||||
AND control_id = @control_id;
|
||||
`
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": sac.StateOfApplicabilityID,
|
||||
"control_id": sac.ControlID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
return err
|
||||
}
|
||||
|
||||
func (sacs *StateOfApplicabilityControls) LoadByStateOfApplicabilityID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
stateOfApplicabilityID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
state_of_applicability_id,
|
||||
control_id,
|
||||
organization_id,
|
||||
snapshot_id,
|
||||
applicability,
|
||||
justification,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
states_of_applicability_controls
|
||||
WHERE
|
||||
%s
|
||||
AND state_of_applicability_id = @state_of_applicability_id
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": stateOfApplicabilityID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query states_of_applicability_controls: %w", err)
|
||||
}
|
||||
|
||||
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[StateOfApplicabilityControl])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect states_of_applicability_controls: %w", err)
|
||||
}
|
||||
|
||||
*sacs = controls
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sacs *StateOfApplicabilityControls) LoadByControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor[StateOfApplicabilityOrderField],
|
||||
) error {
|
||||
q := `
|
||||
WITH soac_ctrl AS (
|
||||
SELECT
|
||||
soac.id,
|
||||
soac.state_of_applicability_id,
|
||||
soac.control_id,
|
||||
soac.organization_id,
|
||||
soac.snapshot_id,
|
||||
soac.applicability,
|
||||
soac.justification,
|
||||
soac.created_at,
|
||||
soac.updated_at,
|
||||
soac.tenant_id
|
||||
FROM
|
||||
states_of_applicability_controls soac
|
||||
INNER JOIN
|
||||
states_of_applicability soa ON soac.state_of_applicability_id = soa.id
|
||||
WHERE
|
||||
soac.control_id = @control_id
|
||||
AND soa.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
state_of_applicability_id,
|
||||
control_id,
|
||||
organization_id,
|
||||
snapshot_id,
|
||||
applicability,
|
||||
justification,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
soac_ctrl
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"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 state_of_applicability_controls: %w", err)
|
||||
}
|
||||
|
||||
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[StateOfApplicabilityControl])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect state_of_applicability_controls: %w", err)
|
||||
}
|
||||
|
||||
*sacs = controls
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sacs *StateOfApplicabilityControls) CountByControlID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH soac_ctrl AS (
|
||||
SELECT
|
||||
soac.id,
|
||||
soac.organization_id,
|
||||
soac.tenant_id
|
||||
FROM
|
||||
states_of_applicability_controls soac
|
||||
WHERE
|
||||
soac.control_id = @control_id
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
soac_ctrl
|
||||
WHERE
|
||||
%s;
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.NamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
var count int
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return 0, fmt.Errorf("cannot scan count: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (acfs *AvailableStateOfApplicabilityControls) LoadAvailableByStateOfApplicabilityID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
stateOfApplicabilityID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
WITH soa_info AS (
|
||||
SELECT
|
||||
soa.organization_id,
|
||||
soa.tenant_id AS soa_tenant_id
|
||||
FROM states_of_applicability soa
|
||||
WHERE soa.tenant_id = @tenant_id
|
||||
AND soa.id = @state_of_applicability_id
|
||||
),
|
||||
filtered_controls AS (
|
||||
SELECT
|
||||
c.id AS control_id,
|
||||
c.section_title,
|
||||
c.name,
|
||||
c.framework_id,
|
||||
c.organization_id,
|
||||
c.tenant_id,
|
||||
c.best_practice
|
||||
FROM controls c
|
||||
WHERE %s
|
||||
),
|
||||
all_controls AS (
|
||||
SELECT
|
||||
fc.control_id,
|
||||
fc.section_title,
|
||||
fc.name,
|
||||
fc.framework_id,
|
||||
fc.organization_id,
|
||||
fc.tenant_id,
|
||||
f.name AS framework_name,
|
||||
fc.best_practice
|
||||
FROM filtered_controls fc
|
||||
INNER JOIN frameworks f ON fc.framework_id = f.id
|
||||
CROSS JOIN soa_info si
|
||||
WHERE fc.organization_id = si.organization_id
|
||||
),
|
||||
existing_links AS (
|
||||
SELECT
|
||||
soac.control_id,
|
||||
soac.state_of_applicability_id,
|
||||
soac.applicability,
|
||||
soac.justification
|
||||
FROM states_of_applicability_controls soac
|
||||
CROSS JOIN soa_info si
|
||||
WHERE soac.tenant_id = si.soa_tenant_id
|
||||
AND soac.state_of_applicability_id = @state_of_applicability_id
|
||||
),
|
||||
regulatory_controls AS (
|
||||
SELECT DISTINCT co.control_id
|
||||
FROM controls_obligations co
|
||||
INNER JOIN obligations o ON o.id = co.obligation_id
|
||||
CROSS JOIN soa_info si
|
||||
WHERE co.tenant_id = si.soa_tenant_id
|
||||
AND o.tenant_id = si.soa_tenant_id
|
||||
AND o.type = 'LEGAL'
|
||||
),
|
||||
contractual_controls AS (
|
||||
SELECT DISTINCT co.control_id
|
||||
FROM controls_obligations co
|
||||
INNER JOIN obligations o ON o.id = co.obligation_id
|
||||
CROSS JOIN soa_info si
|
||||
WHERE co.tenant_id = si.soa_tenant_id
|
||||
AND o.tenant_id = si.soa_tenant_id
|
||||
AND o.type = 'CONTRACTUAL'
|
||||
),
|
||||
risk_controls AS (
|
||||
SELECT DISTINCT cm.control_id
|
||||
FROM controls_measures cm
|
||||
INNER JOIN risks_measures rm ON rm.measure_id = cm.measure_id
|
||||
CROSS JOIN soa_info si
|
||||
WHERE cm.tenant_id = si.soa_tenant_id
|
||||
AND rm.tenant_id = si.soa_tenant_id
|
||||
)
|
||||
SELECT
|
||||
ac.control_id,
|
||||
ac.section_title,
|
||||
ac.name,
|
||||
ac.framework_id,
|
||||
ac.organization_id,
|
||||
ac.framework_name,
|
||||
el.state_of_applicability_id,
|
||||
el.applicability,
|
||||
el.justification,
|
||||
ac.best_practice,
|
||||
CASE WHEN reg.control_id IS NOT NULL THEN TRUE ELSE FALSE END AS regulatory,
|
||||
CASE WHEN cont.control_id IS NOT NULL THEN TRUE ELSE FALSE END AS contractual,
|
||||
CASE WHEN risk.control_id IS NOT NULL THEN TRUE ELSE FALSE END AS risk_assessment
|
||||
FROM all_controls ac
|
||||
LEFT JOIN existing_links el ON ac.control_id = el.control_id
|
||||
LEFT JOIN regulatory_controls reg ON reg.control_id = ac.control_id
|
||||
LEFT JOIN contractual_controls cont ON cont.control_id = ac.control_id
|
||||
LEFT JOIN risk_controls risk ON risk.control_id = ac.control_id
|
||||
ORDER BY ac.framework_name, ac.section_title, ac.name
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_of_applicability_id": stateOfApplicabilityID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query available controls: %w", err)
|
||||
}
|
||||
|
||||
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[AvailableStateOfApplicabilityControl])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect available controls: %w", err)
|
||||
}
|
||||
|
||||
*acfs = controls
|
||||
return nil
|
||||
}
|
||||
61
pkg/coredata/state_of_applicability_filter.go
Normal file
61
pkg/coredata/state_of_applicability_filter.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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 (
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type (
|
||||
StateOfApplicabilityFilter struct {
|
||||
snapshotID **gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewStateOfApplicabilityFilter(snapshotID **gid.GID) *StateOfApplicabilityFilter {
|
||||
return &StateOfApplicabilityFilter{
|
||||
snapshotID: snapshotID,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *StateOfApplicabilityFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args := pgx.StrictNamedArgs{}
|
||||
|
||||
if f.snapshotID == nil {
|
||||
args["has_snapshot_filter"] = false
|
||||
args["filter_snapshot_id"] = nil
|
||||
} else if *f.snapshotID == nil {
|
||||
args["has_snapshot_filter"] = true
|
||||
args["filter_snapshot_id"] = nil
|
||||
} else {
|
||||
args["has_snapshot_filter"] = true
|
||||
args["filter_snapshot_id"] = **f.snapshotID
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (f *StateOfApplicabilityFilter) SQLFragment() string {
|
||||
return `
|
||||
CASE
|
||||
WHEN @has_snapshot_filter::boolean = false THEN TRUE
|
||||
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NOT NULL THEN
|
||||
snapshot_id = @filter_snapshot_id::text
|
||||
WHEN @has_snapshot_filter::boolean = true AND @filter_snapshot_id::text IS NULL THEN
|
||||
snapshot_id IS NULL
|
||||
ELSE TRUE
|
||||
END`
|
||||
}
|
||||
62
pkg/coredata/state_of_applicability_order_field.go
Normal file
62
pkg/coredata/state_of_applicability_order_field.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
StateOfApplicabilityOrderField string
|
||||
)
|
||||
|
||||
const (
|
||||
StateOfApplicabilityOrderFieldName StateOfApplicabilityOrderField = "NAME"
|
||||
StateOfApplicabilityOrderFieldCreatedAt StateOfApplicabilityOrderField = "CREATED_AT"
|
||||
)
|
||||
|
||||
func (s StateOfApplicabilityOrderField) Column() string {
|
||||
switch s {
|
||||
case StateOfApplicabilityOrderFieldName:
|
||||
return "name"
|
||||
case StateOfApplicabilityOrderFieldCreatedAt:
|
||||
return "created_at"
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported order by: %s", s))
|
||||
}
|
||||
|
||||
func (s StateOfApplicabilityOrderField) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s StateOfApplicabilityOrderField) IsValid() bool {
|
||||
switch s {
|
||||
case StateOfApplicabilityOrderFieldName, StateOfApplicabilityOrderFieldCreatedAt:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s StateOfApplicabilityOrderField) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s *StateOfApplicabilityOrderField) UnmarshalText(text []byte) error {
|
||||
*s = StateOfApplicabilityOrderField(text)
|
||||
if !s.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid StateOfApplicabilityOrderField", string(text))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user