Change state of applicability

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-01-02 15:04:11 +01:00
parent c60e19a7a8
commit 4ed3f5a067
105 changed files with 18351 additions and 284 deletions

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View File

@@ -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) {

View 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
);

View File

@@ -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

View 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
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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
}

View 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
}

View 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`
}

View 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
}