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

@@ -103,6 +103,7 @@ const (
ActionListMeetings Action = "listMeetings"
ActionListMembers Action = "listMembers"
ActionListNonconformities Action = "listNonconformities"
ActionListStatesOfApplicability Action = "listStatesOfApplicability"
ActionListObligations Action = "listObligations"
ActionListPeople Action = "listPeople"
ActionListProcessingActivities Action = "listProcessingActivities"
@@ -120,43 +121,46 @@ const (
ActionListSignableDocuments Action = "listSignableDocuments"
ActionListSignableDocumentVersion Action = "listSignableDocumentVersion"
ActionCreateAsset Action = "createAsset"
ActionCreateAudit Action = "createAudit"
ActionCreateContinualImprovement Action = "createContinualImprovement"
ActionCreateRightsRequest Action = "createRightsRequest"
ActionCreateControl Action = "createControl"
ActionCreateControlAuditMapping Action = "createControlAuditMapping"
ActionCreateControlDocumentMapping Action = "createControlDocumentMapping"
ActionCreateControlMeasureMapping Action = "createControlMeasureMapping"
ActionCreateControlSnapshotMapping Action = "createControlSnapshotMapping"
ActionCreateCustomDomain Action = "createCustomDomain"
ActionCreateDatum Action = "createDatum"
ActionCreateDocument Action = "createDocument"
ActionCreateDraftDocumentVersion Action = "createDraftDocumentVersion"
ActionCreateFramework Action = "createFramework"
ActionCreateMeasure Action = "createMeasure"
ActionCreateMeeting Action = "createMeeting"
ActionCreateNonconformity Action = "createNonconformity"
ActionCreateObligation Action = "createObligation"
ActionCreatePeople Action = "createPeople"
ActionCreateProcessingActivity Action = "createProcessingActivity"
ActionCreateDataProtectionImpactAssessment Action = "createDataProtectionImpactAssessment"
ActionCreateTransferImpactAssessment Action = "createTransferImpactAssessment"
ActionCreateRisk Action = "createRisk"
ActionCreateRiskDocumentMapping Action = "createRiskDocumentMapping"
ActionCreateRiskMeasureMapping Action = "createRiskMeasureMapping"
ActionCreateRiskObligationMapping Action = "createRiskObligationMapping"
ActionCreateSAMLConfiguration Action = "createSAMLConfiguration"
ActionCreateSnapshot Action = "createSnapshot"
ActionCreateTask Action = "createTask"
ActionCreateTrustCenter Action = "createTrustCenter"
ActionCreateTrustCenterAccess Action = "createTrustCenterAccess"
ActionCreateTrustCenterFile Action = "createTrustCenterFile"
ActionCreateTrustCenterReference Action = "createTrustCenterReference"
ActionCreateVendor Action = "createVendor"
ActionCreateVendorContact Action = "createVendorContact"
ActionCreateVendorRiskAssessment Action = "createVendorRiskAssessment"
ActionCreateVendorService Action = "createVendorService"
ActionCreateAsset Action = "createAsset"
ActionCreateAudit Action = "createAudit"
ActionCreateContinualImprovement Action = "createContinualImprovement"
ActionCreateRightsRequest Action = "createRightsRequest"
ActionCreateControl Action = "createControl"
ActionCreateControlAuditMapping Action = "createControlAuditMapping"
ActionCreateControlObligationMapping Action = "createControlObligationMapping"
ActionCreateControlDocumentMapping Action = "createControlDocumentMapping"
ActionCreateControlMeasureMapping Action = "createControlMeasureMapping"
ActionCreateControlSnapshotMapping Action = "createControlSnapshotMapping"
ActionCreateStateOfApplicabilityControlMapping Action = "createStateOfApplicabilityControlMapping"
ActionCreateCustomDomain Action = "createCustomDomain"
ActionCreateDatum Action = "createDatum"
ActionCreateDocument Action = "createDocument"
ActionCreateDraftDocumentVersion Action = "createDraftDocumentVersion"
ActionCreateFramework Action = "createFramework"
ActionCreateMeasure Action = "createMeasure"
ActionCreateMeeting Action = "createMeeting"
ActionCreateNonconformity Action = "createNonconformity"
ActionCreateObligation Action = "createObligation"
ActionCreatePeople Action = "createPeople"
ActionCreateProcessingActivity Action = "createProcessingActivity"
ActionCreateStateOfApplicability Action = "createStateOfApplicability"
ActionCreateDataProtectionImpactAssessment Action = "createDataProtectionImpactAssessment"
ActionCreateTransferImpactAssessment Action = "createTransferImpactAssessment"
ActionCreateRisk Action = "createRisk"
ActionCreateRiskDocumentMapping Action = "createRiskDocumentMapping"
ActionCreateRiskMeasureMapping Action = "createRiskMeasureMapping"
ActionCreateRiskObligationMapping Action = "createRiskObligationMapping"
ActionCreateSAMLConfiguration Action = "createSAMLConfiguration"
ActionCreateSnapshot Action = "createSnapshot"
ActionCreateTask Action = "createTask"
ActionCreateTrustCenter Action = "createTrustCenter"
ActionCreateTrustCenterAccess Action = "createTrustCenterAccess"
ActionCreateTrustCenterFile Action = "createTrustCenterFile"
ActionCreateTrustCenterReference Action = "createTrustCenterReference"
ActionCreateVendor Action = "createVendor"
ActionCreateVendorContact Action = "createVendorContact"
ActionCreateVendorRiskAssessment Action = "createVendorRiskAssessment"
ActionCreateVendorService Action = "createVendorService"
ActionUpdateAsset Action = "updateAsset"
ActionUpdateAudit Action = "updateAudit"
@@ -175,6 +179,7 @@ const (
ActionUpdateOrganization Action = "updateOrganization"
ActionUpdatePeople Action = "updatePeople"
ActionUpdateProcessingActivity Action = "updateProcessingActivity"
ActionUpdateStateOfApplicability Action = "updateStateOfApplicability"
ActionUpdateDataProtectionImpactAssessment Action = "updateDataProtectionImpactAssessment"
ActionUpdateTransferImpactAssessment Action = "updateTransferImpactAssessment"
ActionUpdateRisk Action = "updateRisk"
@@ -190,50 +195,53 @@ const (
ActionUpdateVendorDataPrivacyAgreement Action = "updateVendorDataPrivacyAgreement"
ActionUpdateVendorService Action = "updateVendorService"
ActionDeleteAsset Action = "deleteAsset"
ActionDeleteAudit Action = "deleteAudit"
ActionDeleteAuditReport Action = "deleteAuditReport"
ActionDeleteContinualImprovement Action = "deleteContinualImprovement"
ActionDeleteRightsRequest Action = "deleteRightsRequest"
ActionDeleteControl Action = "deleteControl"
ActionDeleteControlAuditMapping Action = "deleteControlAuditMapping"
ActionDeleteControlDocumentMapping Action = "deleteControlDocumentMapping"
ActionDeleteControlMeasureMapping Action = "deleteControlMeasureMapping"
ActionDeleteControlSnapshotMapping Action = "deleteControlSnapshotMapping"
ActionDeleteCustomDomain Action = "deleteCustomDomain"
ActionDeleteDatum Action = "deleteDatum"
ActionDeleteDocument Action = "deleteDocument"
ActionDeleteDraftDocumentVersion Action = "deleteDraftDocumentVersion"
ActionDeleteEvidence Action = "deleteEvidence"
ActionDeleteFramework Action = "deleteFramework"
ActionDeleteInvitation Action = "deleteInvitation"
ActionDeleteMeasure Action = "deleteMeasure"
ActionDeleteMeeting Action = "deleteMeeting"
ActionDeleteNonconformity Action = "deleteNonconformity"
ActionDeleteObligation Action = "deleteObligation"
ActionDeleteOrganization Action = "deleteOrganization"
ActionDeleteOrganizationHorizontalLogo Action = "deleteOrganizationHorizontalLogo"
ActionDeletePeople Action = "deletePeople"
ActionDeleteProcessingActivity Action = "deleteProcessingActivity"
ActionDeleteDataProtectionImpactAssessment Action = "deleteDataProtectionImpactAssessment"
ActionDeleteTransferImpactAssessment Action = "deleteTransferImpactAssessment"
ActionDeleteRisk Action = "deleteRisk"
ActionDeleteRiskDocumentMapping Action = "deleteRiskDocumentMapping"
ActionDeleteRiskMeasureMapping Action = "deleteRiskMeasureMapping"
ActionDeleteRiskObligationMapping Action = "deleteRiskObligationMapping"
ActionDeleteSAMLConfiguration Action = "deleteSAMLConfiguration"
ActionDeleteSnapshot Action = "deleteSnapshot"
ActionDeleteTask Action = "deleteTask"
ActionDeleteTrustCenterAccess Action = "deleteTrustCenterAccess"
ActionDeleteTrustCenterFile Action = "deleteTrustCenterFile"
ActionDeleteTrustCenterNDA Action = "deleteTrustCenterNDA"
ActionDeleteTrustCenterReference Action = "deleteTrustCenterReference"
ActionDeleteVendor Action = "deleteVendor"
ActionDeleteVendorBusinessAssociateAgreement Action = "deleteVendorBusinessAssociateAgreement"
ActionDeleteVendorComplianceReport Action = "deleteVendorComplianceReport"
ActionDeleteVendorContact Action = "deleteVendorContact"
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
ActionDeleteVendorService Action = "deleteVendorService"
ActionDeleteAsset Action = "deleteAsset"
ActionDeleteAudit Action = "deleteAudit"
ActionDeleteAuditReport Action = "deleteAuditReport"
ActionDeleteContinualImprovement Action = "deleteContinualImprovement"
ActionDeleteRightsRequest Action = "deleteRightsRequest"
ActionDeleteControl Action = "deleteControl"
ActionDeleteControlAuditMapping Action = "deleteControlAuditMapping"
ActionDeleteControlObligationMapping Action = "deleteControlObligationMapping"
ActionDeleteControlDocumentMapping Action = "deleteControlDocumentMapping"
ActionDeleteControlMeasureMapping Action = "deleteControlMeasureMapping"
ActionDeleteControlSnapshotMapping Action = "deleteControlSnapshotMapping"
ActionDeleteStateOfApplicabilityControlMapping Action = "deleteStateOfApplicabilityControlMapping"
ActionDeleteCustomDomain Action = "deleteCustomDomain"
ActionDeleteDatum Action = "deleteDatum"
ActionDeleteDocument Action = "deleteDocument"
ActionDeleteDraftDocumentVersion Action = "deleteDraftDocumentVersion"
ActionDeleteEvidence Action = "deleteEvidence"
ActionDeleteFramework Action = "deleteFramework"
ActionDeleteInvitation Action = "deleteInvitation"
ActionDeleteMeasure Action = "deleteMeasure"
ActionDeleteMeeting Action = "deleteMeeting"
ActionDeleteNonconformity Action = "deleteNonconformity"
ActionDeleteObligation Action = "deleteObligation"
ActionDeleteOrganization Action = "deleteOrganization"
ActionDeleteOrganizationHorizontalLogo Action = "deleteOrganizationHorizontalLogo"
ActionDeletePeople Action = "deletePeople"
ActionDeleteProcessingActivity Action = "deleteProcessingActivity"
ActionDeleteStateOfApplicability Action = "deleteStateOfApplicability"
ActionDeleteDataProtectionImpactAssessment Action = "deleteDataProtectionImpactAssessment"
ActionDeleteTransferImpactAssessment Action = "deleteTransferImpactAssessment"
ActionDeleteRisk Action = "deleteRisk"
ActionDeleteRiskDocumentMapping Action = "deleteRiskDocumentMapping"
ActionDeleteRiskMeasureMapping Action = "deleteRiskMeasureMapping"
ActionDeleteRiskObligationMapping Action = "deleteRiskObligationMapping"
ActionDeleteSAMLConfiguration Action = "deleteSAMLConfiguration"
ActionDeleteSnapshot Action = "deleteSnapshot"
ActionDeleteTask Action = "deleteTask"
ActionDeleteTrustCenterAccess Action = "deleteTrustCenterAccess"
ActionDeleteTrustCenterFile Action = "deleteTrustCenterFile"
ActionDeleteTrustCenterNDA Action = "deleteTrustCenterNDA"
ActionDeleteTrustCenterReference Action = "deleteTrustCenterReference"
ActionDeleteVendor Action = "deleteVendor"
ActionDeleteVendorBusinessAssociateAgreement Action = "deleteVendorBusinessAssociateAgreement"
ActionDeleteVendorComplianceReport Action = "deleteVendorComplianceReport"
ActionDeleteVendorContact Action = "deleteVendorContact"
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
ActionDeleteVendorService Action = "deleteVendorService"
ActionAcceptInvitation Action = "acceptInvitation"
ActionAssessVendor Action = "assessVendor"
@@ -315,16 +323,17 @@ var Permissions = map[uint16]map[Action][]Role{
ActionConfirmEmail: NonEmployeeRoles,
ActionAcceptInvitation: NonEmployeeRoles,
ActionListTrustCenterFiles: CoreRoles,
ActionGetTrustCenter: CoreRoles,
ActionMemberships: CoreRoles,
ActionListMembers: CoreRoles,
ActionListInvitations: CoreRoles,
ActionListSlackConnections: CoreRoles,
ActionGetCustomDomain: CoreRoles,
ActionListSAMLConfigurations: CoreRoles,
ActionListMeetings: CoreRoles,
ActionListTasks: CoreRoles,
ActionListTrustCenterFiles: CoreRoles,
ActionGetTrustCenter: CoreRoles,
ActionMemberships: CoreRoles,
ActionListMembers: CoreRoles,
ActionListInvitations: CoreRoles,
ActionListSlackConnections: CoreRoles,
ActionGetCustomDomain: CoreRoles,
ActionListSAMLConfigurations: CoreRoles,
ActionListMeetings: CoreRoles,
ActionListStatesOfApplicability: NonEmployeeRoles,
ActionListTasks: CoreRoles,
ActionUpdateOrganization: EditRoles,
ActionDeleteOrganizationHorizontalLogo: EditRoles,
@@ -339,6 +348,7 @@ var Permissions = map[uint16]map[Action][]Role{
ActionCreateMeasure: EditRoles,
ActionImportMeasure: EditRoles,
ActionCreateMeeting: EditRoles,
ActionCreateStateOfApplicability: EditRoles,
ActionCreateTask: EditRoles,
ActionCreateRisk: EditRoles,
ActionCreateDocument: EditRoles,
@@ -498,23 +508,32 @@ var Permissions = map[uint16]map[Action][]Role{
ActionExportFramework: EditRoles,
},
coredata.ControlEntityType: {
ActionGet: NonEmployeeRoles,
ActionGetFramework: NonEmployeeRoles,
ActionListMeasures: NonEmployeeRoles,
ActionListDocuments: NonEmployeeRoles,
ActionListAudits: NonEmployeeRoles,
ActionListSnapshots: NonEmployeeRoles,
ActionGet: NonEmployeeRoles,
ActionGetFramework: NonEmployeeRoles,
ActionListMeasures: NonEmployeeRoles,
ActionListDocuments: NonEmployeeRoles,
ActionListAudits: NonEmployeeRoles,
ActionListObligations: NonEmployeeRoles,
ActionListSnapshots: NonEmployeeRoles,
ActionListStatesOfApplicability: NonEmployeeRoles,
ActionUpdateControl: EditRoles,
ActionDeleteControl: EditRoles,
ActionCreateControlMeasureMapping: EditRoles,
ActionCreateControlDocumentMapping: EditRoles,
ActionDeleteControlMeasureMapping: EditRoles,
ActionDeleteControlDocumentMapping: EditRoles,
ActionCreateControlAuditMapping: EditRoles,
ActionDeleteControlAuditMapping: EditRoles,
ActionCreateControlSnapshotMapping: EditRoles,
ActionDeleteControlSnapshotMapping: EditRoles,
ActionUpdateControl: EditRoles,
ActionDeleteControl: EditRoles,
ActionCreateControlMeasureMapping: EditRoles,
ActionCreateControlDocumentMapping: EditRoles,
ActionDeleteControlMeasureMapping: EditRoles,
ActionDeleteControlDocumentMapping: EditRoles,
ActionCreateControlAuditMapping: EditRoles,
ActionDeleteControlAuditMapping: EditRoles,
ActionCreateControlObligationMapping: EditRoles,
ActionDeleteControlObligationMapping: EditRoles,
ActionCreateControlSnapshotMapping: EditRoles,
ActionCreateStateOfApplicabilityControlMapping: EditRoles,
ActionDeleteStateOfApplicabilityControlMapping: EditRoles,
ActionDeleteControlSnapshotMapping: EditRoles,
},
coredata.StateOfApplicabilityControlEntityType: {
ActionDeleteStateOfApplicabilityControlMapping: EditRoles,
},
coredata.MeasureEntityType: {
ActionListTasks: CoreRoles,
@@ -689,6 +708,7 @@ var Permissions = map[uint16]map[Action][]Role{
coredata.RightsRequestEntityType: {
ActionGet: NonEmployeeRoles,
ActionGetOrganization: NonEmployeeRoles,
ActionListControls: NonEmployeeRoles,
ActionUpdateRightsRequest: EditRoles,
ActionDeleteRightsRequest: EditRoles,
@@ -763,6 +783,16 @@ var Permissions = map[uint16]map[Action][]Role{
ActionUpdateMeeting: EditRoles,
ActionDeleteMeeting: EditRoles,
},
coredata.StateOfApplicabilityEntityType: {
ActionGet: NonEmployeeRoles,
ActionGetOrganization: NonEmployeeRoles,
ActionListControls: NonEmployeeRoles,
ActionTotalCount: NonEmployeeRoles,
ActionUpdateStateOfApplicability: EditRoles,
ActionDeleteStateOfApplicability: EditRoles,
ActionDeleteStateOfApplicabilityControlMapping: EditRoles,
},
}
func GetPermissionsForAction(entityType uint16, action Action) []Role {

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
}

View File

@@ -47,13 +47,34 @@ var (
//go:embed transfer_impact_assessments_template.html
transferImpactAssessmentsTemplateContent string
//go:embed soa_template.html
soaTemplateContent string
templateFuncs = template.FuncMap{
"now": func() time.Time { return time.Now() },
"eq": func(a, b any) bool { return a == b },
"add": func(a, b int) int { return a + b },
"string": func(v fmt.Stringer) string { return v.String() },
"lower": func(s string) string { return strings.ToLower(s) },
"add": func(a, b int) int { return a + b },
"classificationString": func(c Classification) string { return string(c) },
"boolToYesNo": func(b *bool) string {
if b == nil {
return ""
}
if *b {
return "yes"
}
return "no"
},
"boolToYesNoDash": func(b *bool) string {
if b == nil {
return "-"
}
if *b {
return "Yes"
}
return "No"
},
"formatContent": func(content string) template.HTML {
md := goldmark.New(
goldmark.WithExtensions(extension.Table),
@@ -176,6 +197,8 @@ var (
dataProtectionImpactAssessmentsTemplate = template.Must(template.New("dataProtectionImpactAssessments").Funcs(templateFuncs).Parse(dataProtectionImpactAssessmentsTemplateContent))
transferImpactAssessmentsTemplate = template.Must(template.New("transferImpactAssessments").Funcs(templateFuncs).Parse(transferImpactAssessmentsTemplateContent))
stateOfApplicabilityTemplate = template.Must(template.New("state-of-applicability").Funcs(templateFuncs).Parse(soaTemplateContent))
)
type (
@@ -264,6 +287,35 @@ type (
LocalLawRisk *string
SupplementaryMeasures *string
}
StateOfApplicabilityData struct {
Title string
OrganizationName string
CreatedAt time.Time
TotalControls int
FrameworkGroups []FrameworkControlGroup
CompanyHorizontalLogoBase64 string
Version int
PublishedAt time.Time
Approver string
}
FrameworkControlGroup struct {
FrameworkName string
Controls []ControlData
}
ControlData struct {
FrameworkName string
SectionTitle string
Name string
Applicability *bool
Justification *string
BestPractice bool
Regulatory *bool
Contractual *bool
RiskAssessment *bool
}
)
const (
@@ -308,3 +360,12 @@ func RenderTransferImpactAssessmentsTableHTML(data TransferImpactAssessmentTable
return buf.Bytes(), nil
}
func RenderStateOfApplicabilityHTML(data StateOfApplicabilityData) ([]byte, error) {
var buf bytes.Buffer
if err := stateOfApplicabilityTemplate.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("cannot execute SOA template: %w", err)
}
return buf.Bytes(), nil
}

View File

@@ -20,9 +20,9 @@ import (
"testing"
"time"
"go.probo.inc/probo/pkg/coredata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/coredata"
)
func TestRenderHTML(t *testing.T) {
@@ -198,6 +198,8 @@ func TestTemplateFunctions(t *testing.T) {
eqFunc := templateFuncs["eq"].(func(any, any) bool)
assert.True(t, eqFunc("test", "test"))
assert.False(t, eqFunc("test", "other"))
assert.True(t, eqFunc(0, 0))
assert.False(t, eqFunc(0, 1))
})
t.Run("lower function", func(t *testing.T) {

View File

@@ -0,0 +1,480 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>State of Applicability</title>
<style>
@page {
size: A4 landscape;
margin: 2.5cm;
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 9pt;
color: #666;
}
}
body {
font-family: Arial, sans-serif;
font-size: 7.5pt;
line-height: 1.4;
color: #333;
margin: 0;
padding: 0;
background: white;
}
/* Cover page */
.cover-page {
page-break-after: always;
}
.company-header {
margin-bottom: 30px;
page-break-after: avoid;
}
.company-logo {
max-height: 50px;
max-width: 250px;
object-fit: contain;
display: block;
}
.export-title {
font-size: 22pt;
font-weight: normal;
color: #1a1a1a;
margin: 0 0 25px 0;
text-align: left;
}
.export-subtitle {
font-size: 18pt;
font-weight: normal;
color: #1a1a1a;
margin: 0 0 25px 0;
text-align: left;
}
.document-meta {
margin: 0 0 30px 0;
font-size: 9pt;
}
.meta-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #333;
}
.meta-table td {
padding: 6px 8px;
border: 1px solid #333;
vertical-align: middle;
}
.meta-table td:first-child {
font-weight: 600;
width: 25%;
background: #f8f8f8;
color: #333;
}
.classification {
font-weight: bold;
text-transform: uppercase;
}
.purpose-section {
margin: 30px 0;
page-break-after: avoid;
}
.purpose-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.purpose-text {
font-size: 10pt;
color: #333;
line-height: 1.5;
text-align: justify;
}
/* Controls page */
.controls-page {
page-break-before: always;
}
.controls-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.controls-table {
width: 100%;
border-collapse: collapse;
font-size: 8pt;
margin-top: 10px;
}
.controls-table th,
.controls-table td {
padding: 5px 6px;
text-align: left;
border: 1px solid #ddd;
vertical-align: top;
}
.controls-table th {
background: #f5f5f5;
font-weight: bold;
color: #333;
}
.controls-table tr {
page-break-inside: avoid;
}
.section-tag {
display: inline-block;
background: #e0e0e0;
color: #333;
padding: 2px 5px;
border-radius: 3px;
font-size: 7pt;
font-weight: 500;
margin-right: 5px;
}
.state-tag {
display: inline-block;
padding: 2px 5px;
border-radius: 4px;
font-size: 8pt;
font-weight: 500;
}
.state-tag-success {
background: #eefadc;
color: #5d770d;
}
.state-tag-warning {
background: #fff4d5;
color: #ad5700;
}
.state-tag-danger {
background: #ffefef;
color: #cd2b31;
}
/* Annex page */
.annex-page {
page-break-before: always;
}
.annex-title {
font-size: 15pt;
font-weight: bold;
color: #000;
margin: 0 0 15px 0;
page-break-after: avoid;
}
.annex-section {
margin-bottom: 20px;
}
.annex-section-title {
font-size: 13pt;
font-weight: bold;
color: #000;
margin: 15px 0 10px 0;
}
.annex-subsection-title {
font-size: 10pt;
font-weight: bold;
color: #333;
margin: 12px 0 8px 0;
}
.annex-enum-list {
margin: 10px 0;
padding-left: 20px;
}
.annex-enum-item {
margin-bottom: 8px;
font-size: 10pt;
line-height: 1.5;
}
.annex-enum-name {
font-weight: 600;
color: #333;
}
.annex-enum-description {
color: #000;
margin-left: 5px;
}
/* Prevent bad page breaks */
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
page-break-inside: avoid;
}
@media print {
body {
background: white;
}
}
</style>
</head>
<body>
<div class="cover-page">
<div class="company-header">
{{- if .CompanyHorizontalLogoBase64}}
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
{{- end}}
</div>
<h1 class="export-title">State of Applicability</h1>
<h2 class="export-subtitle">{{.Title}}</h2>
<div class="document-meta">
<table class="meta-table">
<tr>
<td>Classification</td>
<td>
<span class="classification">CONFIDENTIAL</span>
</td>
</tr>
<tr>
<td>Approver</td>
<td>{{.Approver}}</td>
</tr>
<tr>
<td>Version</td>
<td>{{.Version}}</td>
</tr>
<tr>
<td>Published</td>
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
</tr>
</table>
</div>
<div class="purpose-section">
<div class="purpose-title">1. Purpose</div>
<div class="purpose-text">
This document provides a comprehensive overview of the state of applicability for controls within the organization.
It serves as a record of which controls are applicable or not applicable to the organization, along with their
relationships to regulatory requirements, contractual obligations, risk assessments, and best practices.
</div>
</div>
</div>
{{- if .FrameworkGroups}}
<div class="controls-page">
<h1 class="controls-title">2. Controls</h1>
<table class="controls-table">
<thead>
<tr>
<th rowspan="2" style="width: 12%;">Framework</th>
<th rowspan="2" style="width: 24%;">Control</th>
<th rowspan="2" style="width: 9%;">Applicability</th>
<th rowspan="2" style="width: 17%;">Justification</th>
<th colspan="4" style="width: 38%; text-align: center;">Justification for inclusion</th>
</tr>
<tr>
<th style="width: 8%;">Regulatory</th>
<th style="width: 8%;">Contractual</th>
<th style="width: 10%;">Best Practice</th>
<th style="width: 12%;">Risk Assessment</th>
</tr>
</thead>
<tbody>
{{- range $group := .FrameworkGroups}}
{{- range $group.Controls}}
<tr>
<td>{{$group.FrameworkName}}</td>
<td><span class="section-tag">{{.SectionTitle}}</span>{{.Name}}</td>
<td>
{{- $state := boolToYesNo .Applicability}}
{{- if eq $state "yes"}}
<span class="state-tag state-tag-success">Yes</span>
{{- else if eq $state "no"}}
<span class="state-tag state-tag-danger">No</span>
{{- else}}
<span class="state-tag">-</span>
{{- end}}
</td>
<td>
{{- if .Justification}}
{{.Justification}}
{{- else}}
-
{{- end}}
</td>
<td>{{boolToYesNoDash .Regulatory}}</td>
<td>{{boolToYesNoDash .Contractual}}</td>
<td>
{{- if .Applicability}}
{{- if and .Applicability .BestPractice}}
Yes
{{- else if .Applicability}}
No
{{- else}}
-
{{- end}}
{{- else}}
-
{{- end}}
</td>
<td>{{boolToYesNoDash .RiskAssessment}}</td>
</tr>
{{- end}}
{{- end}}
</tbody>
</table>
</div>
{{- end}}
<div class="annex-page">
<h1 class="annex-title">3. Annexes</h1>
<div class="annex-section">
<div class="annex-section-title">3.1 Column Definitions</div>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Framework</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-description">The name of the compliance framework or standard to which the control belongs (e.g., ISO 27001, SOC 2, GDPR).</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Control</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-description">The specific control identifier and name within the framework, including its section reference.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Applicability</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The control is applicable to the organization.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The control is not applicable to the organization (with justification provided).</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Justification</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-description">Provides the rationale when a control is not applicable. This field is empty for applicable controls.</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Justification for inclusion</div>
<div class="annex-enum-description" style="margin-bottom: 12px;">
For applicable controls, this section provides additional context on why the control is included, based on regulatory requirements, contractual obligations, best practices, or risk assessments.
</div>
<div style="margin-left: 20px;">
<div class="annex-subsection-title" style="font-size: 9pt; margin-top: 10px;">Regulatory</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The control is linked to one or more legal or regulatory obligations.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The control is not associated with any legal or regulatory obligations.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">-:</span>
<span class="annex-enum-description">Not applicable (control is not applicable).</span>
</li>
</ul>
<div class="annex-subsection-title" style="font-size: 9pt; margin-top: 10px;">Contractual</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The control is linked to one or more contractual obligations.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The control is not associated with any contractual obligations.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">-:</span>
<span class="annex-enum-description">Not applicable (control is not applicable).</span>
</li>
</ul>
<div class="annex-subsection-title" style="font-size: 9pt; margin-top: 10px;">Best Practice</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The control is designated as a best practice recommendation.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The control is not designated as a best practice.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">-:</span>
<span class="annex-enum-description">Not applicable (control is not applicable).</span>
</li>
</ul>
<div class="annex-subsection-title" style="font-size: 9pt; margin-top: 10px;">Risk Assessment</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The control is associated with one or more identified risks through risk mitigation measures.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The control is not currently associated with any identified risks.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">-:</span>
<span class="annex-enum-description">Not applicable (control is not applicable).</span>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>

View File

@@ -39,6 +39,7 @@ type (
SectionTitle string
Status *coredata.ControlStatus
ExclusionJustification *string
BestPractice bool
}
UpdateControlRequest struct {
@@ -48,6 +49,7 @@ type (
SectionTitle *string
Status *coredata.ControlStatus
ExclusionJustification *string
BestPractice *bool
}
)
@@ -587,6 +589,81 @@ func (s ControlService) DeleteAuditMapping(
return control, audit, nil
}
func (s ControlService) CreateObligationMapping(
ctx context.Context,
controlID gid.GID,
obligationID gid.GID,
) (*coredata.Control, *coredata.Obligation, error) {
control := &coredata.Control{}
obligation := &coredata.Obligation{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil {
return fmt.Errorf("cannot load control: %w", err)
}
if err := obligation.LoadByID(ctx, conn, s.svc.scope, obligationID); err != nil {
return fmt.Errorf("cannot load obligation: %w", err)
}
controlObligation := &coredata.ControlObligation{
ControlID: controlID,
ObligationID: obligationID,
CreatedAt: time.Now(),
}
if err := controlObligation.Upsert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot create control obligation mapping: %w", err)
}
return nil
},
)
if err != nil {
return nil, nil, err
}
return control, obligation, nil
}
func (s ControlService) DeleteObligationMapping(
ctx context.Context,
controlID gid.GID,
obligationID gid.GID,
) (*coredata.Control, *coredata.Obligation, error) {
control := &coredata.Control{}
obligation := &coredata.Obligation{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil {
return fmt.Errorf("cannot load control: %w", err)
}
if err := obligation.LoadByID(ctx, conn, s.svc.scope, obligationID); err != nil {
return fmt.Errorf("cannot load obligation: %w", err)
}
controlObligation := &coredata.ControlObligation{}
if err := controlObligation.Delete(ctx, conn, s.svc.scope, control.ID, obligation.ID); err != nil {
return fmt.Errorf("cannot delete control obligation mapping: %w", err)
}
return nil
},
)
if err != nil {
return nil, nil, fmt.Errorf("cannot delete control obligation mapping: %w", err)
}
return control, obligation, nil
}
func (s ControlService) ListForAuditID(
ctx context.Context,
auditID gid.GID,
@@ -723,6 +800,63 @@ func (s ControlService) ListForSnapshotID(
return page.NewPage([]*coredata.Control(controls), cursor), nil
}
func (s ControlService) CountForStateOfApplicabilityID(
ctx context.Context,
stateOfApplicabilityID gid.GID,
filter *coredata.ControlFilter,
) (int, error) {
var count int
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
controls := &coredata.Controls{}
count, err = controls.CountByStateOfApplicabilityID(ctx, conn, s.svc.scope, stateOfApplicabilityID, filter)
if err != nil {
return fmt.Errorf("cannot count controls: %w", err)
}
return nil
},
)
if err != nil {
return 0, fmt.Errorf("cannot count controls: %w", err)
}
return count, nil
}
func (s ControlService) ListForStateOfApplicabilityID(
ctx context.Context,
stateOfApplicabilityID gid.GID,
cursor *page.Cursor[coredata.ControlOrderField],
filter *coredata.ControlFilter,
) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) {
var controls coredata.Controls
stateOfApplicability := &coredata.StateOfApplicability{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := stateOfApplicability.LoadByID(ctx, conn, s.svc.scope, stateOfApplicabilityID); err != nil {
return fmt.Errorf("cannot load state of applicability: %w", err)
}
if err := controls.LoadByStateOfApplicabilityID(ctx, conn, s.svc.scope, stateOfApplicabilityID, cursor, filter); err != nil {
return fmt.Errorf("cannot load controls: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage([]*coredata.Control(controls), cursor), nil
}
func (s ControlService) Create(
ctx context.Context,
req CreateControlRequest,
@@ -742,6 +876,7 @@ func (s ControlService) Create(
SectionTitle: req.SectionTitle,
Status: *req.Status,
ExclusionJustification: req.ExclusionJustification,
BestPractice: req.BestPractice,
CreatedAt: now,
UpdatedAt: now,
}
@@ -822,6 +957,10 @@ func (s ControlService) Update(
control.ExclusionJustification = req.ExclusionJustification
}
if req.BestPractice != nil {
control.BestPractice = *req.BestPractice
}
control.UpdatedAt = time.Now()
return control.Update(ctx, conn, s.svc.scope)

View File

@@ -329,9 +329,7 @@ func (s *DataProtectionImpactAssessmentService) ExportPDF(
horizontalLogoBase64 := ""
if organization.HorizontalLogoFileID != nil {
fileRecord := &coredata.File{}
fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
})
fileErr := fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
if fileErr == nil {
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
if logoErr == nil {

View File

@@ -1640,9 +1640,7 @@ func exportDocumentPDF(
horizontalLogoBase64 := ""
if organization.HorizontalLogoFileID != nil {
fileRecord := &coredata.File{}
fileErr := svc.pg.WithConn(ctx, func(conn pg.Conn) error {
return fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID)
})
fileErr := fileRecord.LoadByID(ctx, conn, scope, *organization.HorizontalLogoFileID)
if fileErr == nil {
base64Data, mimeType, logoErr := svc.fileManager.GetFileBase64(ctx, fileRecord)
if logoErr == nil {

View File

@@ -72,9 +72,10 @@ type (
Dark string `json:"dark"`
} `json:"logo,omitempty"`
Controls []struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
BestPractice *bool `json:"best_practice,omitempty"`
} `json:"controls"`
}
}
@@ -572,6 +573,10 @@ func (s FrameworkService) Import(
now := time.Now()
description := control.Description
bestPractice := true
if control.BestPractice != nil {
bestPractice = *control.BestPractice
}
control := &coredata.Control{
ID: controlID,
FrameworkID: frameworkID,
@@ -580,6 +585,7 @@ func (s FrameworkService) Import(
Name: control.Name,
Description: &description,
Status: coredata.ControlStatusIncluded,
BestPractice: bestPractice,
CreatedAt: now,
UpdatedAt: now,
}

View File

@@ -41,7 +41,8 @@ type (
OwnerID gid.GID
LastReviewDate *time.Time
DueDate *time.Time
Status *coredata.ObligationStatus
Status coredata.ObligationStatus
Type coredata.ObligationType
}
UpdateObligationRequest struct {
@@ -55,6 +56,7 @@ type (
LastReviewDate **time.Time
DueDate **time.Time
Status *coredata.ObligationStatus
Type *coredata.ObligationType
}
)
@@ -69,6 +71,7 @@ func (cor *CreateObligationRequest) Validate() error {
v.Check(cor.Regulator, "regulator", validator.SafeText(TitleMaxLength))
v.Check(cor.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
v.Check(cor.Status, "status", validator.OneOfSlice(coredata.ObligationStatuses()))
v.Check(cor.Type, "type", validator.OneOfSlice(coredata.ObligationTypes()))
return v.Error()
}
@@ -84,6 +87,7 @@ func (uor *UpdateObligationRequest) Validate() error {
v.Check(uor.Regulator, "regulator", validator.SafeText(NameMaxLength))
v.Check(uor.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
v.Check(uor.Status, "status", validator.OneOfSlice(coredata.ObligationStatuses()))
v.Check(uor.Type, "type", validator.OneOfSlice(coredata.ObligationTypes()))
return v.Error()
}
@@ -133,7 +137,8 @@ func (s *ObligationService) Create(
OwnerID: req.OwnerID,
LastReviewDate: req.LastReviewDate,
DueDate: req.DueDate,
Status: *req.Status,
Status: req.Status,
Type: req.Type,
CreatedAt: now,
UpdatedAt: now,
}
@@ -223,6 +228,10 @@ func (s *ObligationService) Update(
obligation.Status = *req.Status
}
if req.Type != nil {
obligation.Type = *req.Type
}
obligation.UpdatedAt = time.Now()
if err := obligation.Update(ctx, conn, s.svc.scope); err != nil {
@@ -290,6 +299,38 @@ func (s ObligationService) CountForOrganizationID(
return count, nil
}
func (s ObligationService) ListForControlID(
ctx context.Context,
controlID gid.GID,
cursor *page.Cursor[coredata.ObligationOrderField],
filter *coredata.ObligationFilter,
) (*page.Page[*coredata.Obligation, coredata.ObligationOrderField], error) {
var obligations coredata.Obligations
control := &coredata.Control{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := control.LoadByID(ctx, conn, s.svc.scope, controlID); err != nil {
return fmt.Errorf("cannot load control: %w", err)
}
err := obligations.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor, filter)
if err != nil {
return fmt.Errorf("cannot load obligations: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(obligations, cursor), nil
}
func (s ObligationService) ListForOrganizationID(
ctx context.Context,
organizationID gid.GID,

View File

@@ -415,9 +415,7 @@ func (s *ProcessingActivityService) ExportPDF(
horizontalLogoBase64 := ""
if organization.HorizontalLogoFileID != nil {
fileRecord := &coredata.File{}
fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
})
fileErr := fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
if fileErr == nil {
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
if logoErr == nil {

View File

@@ -117,6 +117,7 @@ type (
ProcessingActivities *ProcessingActivityService
DataProtectionImpactAssessments *DataProtectionImpactAssessmentService
TransferImpactAssessments *TransferImpactAssessmentService
StatesOfApplicability *StateOfApplicabilityService
Files *FileService
CustomDomains *CustomDomainService
SlackMessages *slack.SlackMessageService
@@ -264,6 +265,10 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
svc: tenantService,
html2pdfConverter: s.html2pdfConverter,
}
tenantService.StatesOfApplicability = &StateOfApplicabilityService{
svc: tenantService,
html2pdfConverter: s.html2pdfConverter,
}
tenantService.Files = &FileService{svc: tenantService}
tenantService.CustomDomains = &CustomDomainService{
svc: tenantService,

View File

@@ -0,0 +1,589 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package probo
import (
"context"
"fmt"
"io"
"time"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/docgen"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/html2pdf"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/validator"
)
type StateOfApplicabilityService struct {
svc *TenantService
html2pdfConverter *html2pdf.Converter
}
type (
CreateStateOfApplicabilityRequest struct {
OrganizationID gid.GID
Name string
OwnerID gid.GID
}
UpdateStateOfApplicabilityRequest struct {
StateOfApplicabilityID gid.GID
Name *string
OwnerID *gid.GID
}
)
func (csr *CreateStateOfApplicabilityRequest) Validate() error {
v := validator.New()
v.Check(csr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(csr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(csr.OwnerID, "owner_id", validator.Required(), validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (usr *UpdateStateOfApplicabilityRequest) Validate() error {
v := validator.New()
v.Check(usr.StateOfApplicabilityID, "state_of_applicability_id", validator.Required(), validator.GID(coredata.StateOfApplicabilityEntityType))
v.Check(usr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(usr.OwnerID, "owner_id", validator.GID(coredata.PeopleEntityType))
return v.Error()
}
func (s StateOfApplicabilityService) ListForOrganizationID(
ctx context.Context,
organizationID gid.GID,
cursor *page.Cursor[coredata.StateOfApplicabilityOrderField],
filter *coredata.StateOfApplicabilityFilter,
) (*page.Page[*coredata.StateOfApplicability, coredata.StateOfApplicabilityOrderField], error) {
var statesOfApplicability coredata.StatesOfApplicability
organization := &coredata.Organization{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
err := statesOfApplicability.LoadByOrganizationID(
ctx,
conn,
s.svc.scope,
organization.ID,
cursor,
filter,
)
if err != nil {
return fmt.Errorf("cannot load states_of_applicability: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return page.NewPage(statesOfApplicability, cursor), nil
}
func (s StateOfApplicabilityService) CountForOrganizationID(
ctx context.Context,
organizationID gid.GID,
filter *coredata.StateOfApplicabilityFilter,
) (int, error) {
var count int
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) (err error) {
statesOfApplicability := &coredata.StatesOfApplicability{}
count, err = statesOfApplicability.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter)
if err != nil {
return fmt.Errorf("cannot count states_of_applicability: %w", err)
}
return nil
},
)
if err != nil {
return 0, err
}
return count, nil
}
func (s StateOfApplicabilityService) Get(
ctx context.Context,
stateOfApplicabilityID gid.GID,
) (*coredata.StateOfApplicability, error) {
stateOfApplicability := &coredata.StateOfApplicability{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return stateOfApplicability.LoadByID(ctx, conn, s.svc.scope, stateOfApplicabilityID)
},
)
if err != nil {
return nil, err
}
return stateOfApplicability, nil
}
func (s StateOfApplicabilityService) Create(
ctx context.Context,
req CreateStateOfApplicabilityRequest,
) (*coredata.StateOfApplicability, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
now := time.Now()
organization := &coredata.Organization{}
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return organization.LoadByID(ctx, conn, s.svc.scope, req.OrganizationID)
},
)
if err != nil {
return nil, fmt.Errorf("cannot load organization: %w", err)
}
stateOfApplicabilityID := gid.New(organization.ID.TenantID(), coredata.StateOfApplicabilityEntityType)
stateOfApplicability := &coredata.StateOfApplicability{
ID: stateOfApplicabilityID,
OrganizationID: organization.ID,
Name: req.Name,
OwnerID: req.OwnerID,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := stateOfApplicability.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert state_of_applicability: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return stateOfApplicability, nil
}
func (s StateOfApplicabilityService) Update(
ctx context.Context,
req UpdateStateOfApplicabilityRequest,
) (*coredata.StateOfApplicability, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
stateOfApplicability := &coredata.StateOfApplicability{}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := stateOfApplicability.LoadByID(ctx, conn, s.svc.scope, req.StateOfApplicabilityID); err != nil {
return fmt.Errorf("cannot load state_of_applicability: %w", err)
}
if req.Name != nil {
stateOfApplicability.Name = *req.Name
}
if req.OwnerID != nil {
stateOfApplicability.OwnerID = *req.OwnerID
}
stateOfApplicability.UpdatedAt = time.Now()
if err := stateOfApplicability.Update(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot update state_of_applicability: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return stateOfApplicability, nil
}
func (s StateOfApplicabilityService) Delete(
ctx context.Context,
stateOfApplicabilityID gid.GID,
) error {
stateOfApplicability := &coredata.StateOfApplicability{ID: stateOfApplicabilityID}
err := s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
if err := stateOfApplicability.LoadByID(ctx, conn, s.svc.scope, stateOfApplicabilityID); err != nil {
return fmt.Errorf("cannot load state_of_applicability: %w", err)
}
if err := stateOfApplicability.Delete(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot delete state_of_applicability: %w", err)
}
return nil
},
)
if err != nil {
return err
}
return nil
}
func (s StateOfApplicabilityService) ListAvailableControls(
ctx context.Context,
stateOfApplicabilityID gid.GID,
) ([]*coredata.AvailableStateOfApplicabilityControl, error) {
var availableControls coredata.AvailableStateOfApplicabilityControls
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := availableControls.LoadAvailableByStateOfApplicabilityID(ctx, conn, s.svc.scope, stateOfApplicabilityID); err != nil {
return fmt.Errorf("cannot load available controls: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return availableControls, nil
}
func (s StateOfApplicabilityService) LinkControl(
ctx context.Context,
stateOfApplicabilityID gid.GID,
controlID gid.GID,
applicability bool,
justification *string,
) (*coredata.StateOfApplicabilityControl, error) {
stateOfApplicability := &coredata.StateOfApplicability{}
err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
return stateOfApplicability.LoadByID(ctx, conn, s.svc.scope, stateOfApplicabilityID)
})
if err != nil {
return nil, fmt.Errorf("cannot load state of applicability: %w", err)
}
now := time.Now()
control := &coredata.StateOfApplicabilityControl{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.StateOfApplicabilityControlEntityType),
StateOfApplicabilityID: stateOfApplicabilityID,
ControlID: controlID,
OrganizationID: stateOfApplicability.OrganizationID,
Applicability: applicability,
Justification: justification,
CreatedAt: now,
UpdatedAt: now,
}
err = s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
return control.Upsert(ctx, conn, s.svc.scope)
})
if err != nil {
return nil, err
}
return control, nil
}
func (s StateOfApplicabilityService) DeleteControlLink(
ctx context.Context,
stateOfApplicabilityID gid.GID,
controlID gid.GID,
) (gid.GID, error) {
control := &coredata.StateOfApplicabilityControl{}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
if err := control.LoadByStateOfApplicabilityIDAndControlID(ctx, conn, s.svc.scope, stateOfApplicabilityID, controlID); err != nil {
return err
}
return control.Delete(ctx, conn, s.svc.scope)
})
if err != nil {
return gid.GID{}, err
}
return control.ID, nil
}
func (s StateOfApplicabilityService) ListControlLinks(
ctx context.Context,
controlID gid.GID,
cursor *page.Cursor[coredata.StateOfApplicabilityOrderField],
) (*page.Page[*coredata.StateOfApplicabilityControl, coredata.StateOfApplicabilityOrderField], error) {
var controls coredata.StateOfApplicabilityControls
err := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
return controls.LoadByControlID(ctx, conn, s.svc.scope, controlID, cursor)
})
if err != nil {
return nil, err
}
return page.NewPage(controls, cursor), nil
}
func (s StateOfApplicabilityService) ExportPDF(
ctx context.Context,
stateOfApplicabilityID gid.GID,
) ([]byte, error) {
var documentData docgen.StateOfApplicabilityData
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
stateOfApplicability := &coredata.StateOfApplicability{}
if err := stateOfApplicability.LoadByID(ctx, conn, s.svc.scope, stateOfApplicabilityID); err != nil {
return fmt.Errorf("cannot load state of applicability: %w", err)
}
organization := &coredata.Organization{}
if err := organization.LoadByID(ctx, conn, s.svc.scope, stateOfApplicability.OrganizationID); err != nil {
return fmt.Errorf("cannot load organization: %w", err)
}
owner := &coredata.People{}
if err := owner.LoadByID(ctx, conn, s.svc.scope, stateOfApplicability.OwnerID); err != nil {
return fmt.Errorf("cannot load owner: %w", err)
}
var availableControls coredata.AvailableStateOfApplicabilityControls
if err := availableControls.LoadAvailableByStateOfApplicabilityID(ctx, conn, s.svc.scope, stateOfApplicabilityID); err != nil {
return fmt.Errorf("cannot load available controls: %w", err)
}
linkedControls := make([]*coredata.AvailableStateOfApplicabilityControl, 0)
for _, ctrl := range availableControls {
if ctrl.StateOfApplicabilityID != nil {
linkedControls = append(linkedControls, ctrl)
}
}
obligationsByControl := make(map[gid.GID][]coredata.ObligationType)
if len(linkedControls) > 0 {
controlIDs := make([]gid.GID, len(linkedControls))
for i, ctrl := range linkedControls {
controlIDs[i] = ctrl.ControlID
}
var controlObligationTypes coredata.ControlObligationTypes
if err := controlObligationTypes.LoadTypesByControlIDs(ctx, conn, s.svc.scope, controlIDs); err != nil {
return fmt.Errorf("cannot load control obligations: %w", err)
}
for _, cot := range controlObligationTypes {
obligationsByControl[cot.ControlID] = append(obligationsByControl[cot.ControlID], cot.Type)
}
}
controlsWithRisks := make(map[gid.GID]bool)
if len(linkedControls) > 0 {
controlIDs := make([]gid.GID, len(linkedControls))
for i, ctrl := range linkedControls {
controlIDs[i] = ctrl.ControlID
}
var controlsWithRisk coredata.ControlsWithRisk
if err := controlsWithRisk.LoadByControlIDs(ctx, conn, s.svc.scope, controlIDs); err != nil {
return fmt.Errorf("cannot load controls with risks: %w", err)
}
for _, cwr := range controlsWithRisk {
controlsWithRisks[cwr.ControlID] = true
}
}
frameworkControlsMap := make(map[string][]docgen.ControlData)
frameworkOrder := []string{}
for _, ctrl := range linkedControls {
if _, exists := frameworkControlsMap[ctrl.FrameworkName]; !exists {
frameworkOrder = append(frameworkOrder, ctrl.FrameworkName)
frameworkControlsMap[ctrl.FrameworkName] = []docgen.ControlData{}
}
var regulatory *bool
var contractual *bool
var riskAssessment *bool
if ctrl.Applicability != nil && *ctrl.Applicability {
falseVal := false
trueVal := true
regulatory = &falseVal
contractual = &falseVal
riskAssessment = &falseVal
obligations := obligationsByControl[ctrl.ControlID]
for _, obligationType := range obligations {
if obligationType == coredata.ObligationTypeLegal {
regulatory = &trueVal
}
if obligationType == coredata.ObligationTypeContractual {
contractual = &trueVal
}
}
if controlsWithRisks[ctrl.ControlID] {
riskAssessment = &trueVal
}
}
frameworkControlsMap[ctrl.FrameworkName] = append(
frameworkControlsMap[ctrl.FrameworkName],
docgen.ControlData{
FrameworkName: ctrl.FrameworkName,
SectionTitle: ctrl.SectionTitle,
Name: ctrl.Name,
Applicability: ctrl.Applicability,
Justification: ctrl.Justification,
BestPractice: ctrl.BestPractice,
Regulatory: regulatory,
Contractual: contractual,
RiskAssessment: riskAssessment,
},
)
}
frameworkGroups := make([]docgen.FrameworkControlGroup, len(frameworkOrder))
for i, frameworkName := range frameworkOrder {
frameworkGroups[i] = docgen.FrameworkControlGroup{
FrameworkName: frameworkName,
Controls: frameworkControlsMap[frameworkName],
}
}
var snapshots coredata.Snapshots
snapshotType := coredata.SnapshotsTypeStatesOfApplicability
var version int
var publishedAt time.Time
if stateOfApplicability.SnapshotID != nil {
snapshot := &coredata.Snapshot{}
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, *stateOfApplicability.SnapshotID); err != nil {
return fmt.Errorf("cannot load snapshot: %w", err)
}
publishedAt = snapshot.CreatedAt
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType).WithBeforeDate(&snapshot.CreatedAt)
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, stateOfApplicability.OrganizationID, snapshotFilter)
if err != nil {
return fmt.Errorf("cannot count states of applicability snapshots: %w", err)
}
version = snapshotCount
} else {
publishedAt = time.Now()
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType)
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, stateOfApplicability.OrganizationID, snapshotFilter)
if err != nil {
return fmt.Errorf("cannot count states of applicability snapshots: %w", err)
}
version = snapshotCount + 1
}
horizontalLogoBase64 := ""
if organization.HorizontalLogoFileID != nil {
fileRecord := &coredata.File{}
fileErr := fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
if fileErr == nil {
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
if logoErr == nil {
horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
}
}
}
documentData = docgen.StateOfApplicabilityData{
Title: stateOfApplicability.Name,
OrganizationName: organization.Name,
CreatedAt: stateOfApplicability.CreatedAt,
TotalControls: len(linkedControls),
FrameworkGroups: frameworkGroups,
CompanyHorizontalLogoBase64: horizontalLogoBase64,
Version: version,
PublishedAt: publishedAt,
Approver: owner.FullName,
}
return nil
},
)
if err != nil {
return nil, err
}
htmlData, err := docgen.RenderStateOfApplicabilityHTML(documentData)
if err != nil {
return nil, fmt.Errorf("cannot render HTML: %w", err)
}
cfg := html2pdf.RenderConfig{
PageFormat: html2pdf.PageFormatA4,
Orientation: html2pdf.OrientationPortrait,
MarginTop: html2pdf.NewMarginInches(1.0),
MarginBottom: html2pdf.NewMarginInches(1.0),
MarginLeft: html2pdf.NewMarginInches(1.0),
MarginRight: html2pdf.NewMarginInches(1.0),
PrintBackground: true,
Scale: 1.0,
}
pdfReader, err := s.html2pdfConverter.GeneratePDF(ctx, htmlData, cfg)
if err != nil {
return nil, fmt.Errorf("cannot generate PDF: %w", err)
}
pdfData, err := io.ReadAll(pdfReader)
if err != nil {
return nil, fmt.Errorf("cannot read PDF data: %w", err)
}
return pdfData, nil
}

View File

@@ -329,9 +329,7 @@ func (s *TransferImpactAssessmentService) ExportPDF(
horizontalLogoBase64 := ""
if organization.HorizontalLogoFileID != nil {
fileRecord := &coredata.File{}
fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
})
fileErr := fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
if fileErr == nil {
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
if logoErr == nil {

View File

@@ -224,6 +224,14 @@ enum ObligationStatus
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationStatusCompliant")
}
enum ObligationType
@goModel(model: "go.probo.inc/probo/pkg/coredata.ObligationType") {
LEGAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeLegal")
CONTRACTUAL
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeContractual")
}
enum ContinualImprovementStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.ContinualImprovementStatus"
@@ -1235,6 +1243,10 @@ enum SnapshotsType
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeProcessingActivities"
)
STATES_OF_APPLICABILITY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeStatesOfApplicability"
)
}
enum SnapshotOrderField
@@ -1567,6 +1579,10 @@ input DatumFilter {
snapshotId: ID
}
input StateOfApplicabilityFilter {
snapshotId: ID
}
input NonconformityFilter {
snapshotId: ID
}
@@ -1715,6 +1731,15 @@ type Organization implements Node {
orderBy: MeetingOrder
): MeetingConnection! @goField(forceResolver: true)
statesOfApplicability(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: StateOfApplicabilityOrder
filter: StateOfApplicabilityFilter = { snapshotId: null }
): StateOfApplicabilityConnection! @goField(forceResolver: true)
measures(
first: Int
after: CursorKey
@@ -2072,6 +2097,7 @@ type Control implements Node
description: String
status: ControlStatus!
exclusionJustification: String
bestPractice: Boolean!
framework: Framework! @goField(forceResolver: true)
@@ -2101,6 +2127,15 @@ type Control implements Node
orderBy: AuditOrder
): AuditConnection! @goField(forceResolver: true)
obligations(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: ObligationOrder
filter: ObligationFilter
): ObligationConnection! @goField(forceResolver: true)
snapshots(
first: Int
after: CursorKey
@@ -2109,6 +2144,14 @@ type Control implements Node
orderBy: SnapshotOrder
): SnapshotConnection! @goField(forceResolver: true)
stateOfApplicabilityControls(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: StateOfApplicabilityOrder
): StateOfApplicabilityControlConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -2266,6 +2309,26 @@ type Meeting implements Node {
updatedAt: Datetime!
}
type StateOfApplicability implements Node {
id: ID!
name: String!
sourceId: ID
snapshotId: ID
organization: Organization @goField(forceResolver: true)
owner: People! @goField(forceResolver: true)
controls(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: ControlOrder
filter: ControlFilter
): ControlConnection! @goField(forceResolver: true)
availableControls: [AvailableStateOfApplicabilityControl!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type Risk implements Node {
id: ID!
snapshotId: ID
@@ -2381,6 +2444,7 @@ type Obligation implements Node {
lastReviewDate: Datetime
dueDate: Datetime
status: ObligationStatus!
type: ObligationType!
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -2835,6 +2899,20 @@ type MeetingEdge {
node: Meeting!
}
type StateOfApplicabilityConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [StateOfApplicabilityEdge!]!
pageInfo: PageInfo!
}
type StateOfApplicabilityEdge {
cursor: CursorKey!
node: StateOfApplicability!
}
type RiskConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskConnection"
@@ -3191,12 +3269,24 @@ type Mutation {
deleteControlDocumentMapping(
input: DeleteControlDocumentMappingInput!
): DeleteControlDocumentMappingPayload!
createStateOfApplicabilityControlMapping(
input: CreateStateOfApplicabilityControlMappingInput!
): CreateStateOfApplicabilityControlMappingPayload!
deleteStateOfApplicabilityControlMapping(
input: DeleteStateOfApplicabilityControlMappingInput!
): DeleteStateOfApplicabilityControlMappingPayload!
createControlAuditMapping(
input: CreateControlAuditMappingInput!
): CreateControlAuditMappingPayload!
deleteControlAuditMapping(
input: DeleteControlAuditMappingInput!
): DeleteControlAuditMappingPayload!
createControlObligationMapping(
input: CreateControlObligationMappingInput!
): CreateControlObligationMappingPayload!
deleteControlObligationMapping(
input: DeleteControlObligationMappingInput!
): DeleteControlObligationMappingPayload!
createControlSnapshotMapping(
input: CreateControlSnapshotMappingInput!
): CreateControlSnapshotMappingPayload!
@@ -3269,6 +3359,19 @@ type Mutation {
createMeeting(input: CreateMeetingInput!): CreateMeetingPayload!
updateMeeting(input: UpdateMeetingInput!): UpdateMeetingPayload!
deleteMeeting(input: DeleteMeetingInput!): DeleteMeetingPayload!
# StateOfApplicability mutations
createStateOfApplicability(
input: CreateStateOfApplicabilityInput!
): CreateStateOfApplicabilityPayload!
updateStateOfApplicability(
input: UpdateStateOfApplicabilityInput!
): UpdateStateOfApplicabilityPayload!
deleteStateOfApplicability(
input: DeleteStateOfApplicabilityInput!
): DeleteStateOfApplicabilityPayload!
exportStateOfApplicabilityPDF(
input: ExportStateOfApplicabilityPDFInput!
): ExportStateOfApplicabilityPDFPayload!
publishDocumentVersion(
input: PublishDocumentVersionInput!
): PublishDocumentVersionPayload!
@@ -3762,6 +3865,18 @@ input DeleteControlDocumentMappingInput {
documentId: ID!
}
input CreateStateOfApplicabilityControlMappingInput {
stateOfApplicabilityId: ID!
controlId: ID!
applicability: Boolean!
justification: String
}
input DeleteStateOfApplicabilityControlMappingInput {
stateOfApplicabilityId: ID!
controlId: ID!
}
input CreateControlAuditMappingInput {
controlId: ID!
auditId: ID!
@@ -3772,6 +3887,16 @@ input DeleteControlAuditMappingInput {
auditId: ID!
}
input CreateControlObligationMappingInput {
controlId: ID!
obligationId: ID!
}
input DeleteControlObligationMappingInput {
controlId: ID!
obligationId: ID!
}
input CreateControlSnapshotMappingInput {
controlId: ID!
snapshotId: ID!
@@ -3989,6 +4114,95 @@ input DeleteMeetingInput {
meetingId: ID!
}
input CreateStateOfApplicabilityInput {
organizationId: ID!
name: String!
ownerId: ID!
}
input UpdateStateOfApplicabilityInput {
id: ID!
name: String
ownerId: ID
}
input StateOfApplicabilityControlInput {
controlId: ID!
applicability: Boolean!
justification: String
}
type AvailableStateOfApplicabilityControl {
controlId: ID!
sectionTitle: String!
name: String!
frameworkId: ID!
frameworkName: String!
organizationId: ID!
stateOfApplicabilityId: ID
applicability: Boolean
justification: String
bestPractice: Boolean!
regulatory: Boolean!
contractual: Boolean!
riskAssessment: Boolean!
}
input DeleteStateOfApplicabilityInput {
stateOfApplicabilityId: ID!
}
type StateOfApplicabilityControl {
id: ID!
stateOfApplicabilityId: ID!
controlId: ID!
stateOfApplicability: StateOfApplicability! @goField(forceResolver: true)
applicability: Boolean!
justification: String
}
type StateOfApplicabilityControlConnection @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlConnection") {
totalCount: Int!
edges: [StateOfApplicabilityControlEdge!]!
pageInfo: PageInfo!
}
type StateOfApplicabilityControlEdge @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlEdge") {
cursor: CursorKey!
node: StateOfApplicabilityControl!
}
input ExportStateOfApplicabilityPDFInput {
stateOfApplicabilityId: ID!
}
type ExportStateOfApplicabilityPDFPayload {
data: String!
}
input StateOfApplicabilityOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityOrderBy"
) {
direction: OrderDirection!
field: StateOfApplicabilityOrderField!
}
enum StateOfApplicabilityOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.StateOfApplicabilityOrderField"
) {
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.StateOfApplicabilityOrderFieldName"
)
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.StateOfApplicabilityOrderFieldCreatedAt"
)
}
input ConfirmEmailInput {
token: String!
}
@@ -4027,6 +4241,7 @@ input CreateControlInput {
description: String
status: ControlStatus!
exclusionJustification: String
bestPractice: Boolean!
}
input UpdateControlInput {
@@ -4036,6 +4251,7 @@ input UpdateControlInput {
description: String @goField(omittable: true)
status: ControlStatus
exclusionJustification: String
bestPractice: Boolean
}
input DeleteControlInput {
@@ -4119,6 +4335,7 @@ input CreateObligationInput {
lastReviewDate: Datetime
dueDate: Datetime
status: ObligationStatus!
type: ObligationType!
}
input UpdateObligationInput {
@@ -4132,6 +4349,7 @@ input UpdateObligationInput {
lastReviewDate: Datetime @goField(omittable: true)
dueDate: Datetime @goField(omittable: true)
status: ObligationStatus
type: ObligationType
}
input DeleteObligationInput {
@@ -4501,6 +4719,16 @@ type DeleteControlDocumentMappingPayload {
deletedDocumentId: ID!
}
type CreateStateOfApplicabilityControlMappingPayload {
stateOfApplicabilityControlEdge: StateOfApplicabilityControlEdge!
}
type DeleteStateOfApplicabilityControlMappingPayload {
deletedStateOfApplicabilityId: ID!
deletedControlId: ID!
deletedStateOfApplicabilityControlId: ID!
}
type CreateControlAuditMappingPayload {
controlEdge: ControlEdge!
auditEdge: AuditEdge!
@@ -4511,6 +4739,16 @@ type DeleteControlAuditMappingPayload {
deletedAuditId: ID!
}
type CreateControlObligationMappingPayload {
controlEdge: ControlEdge!
obligationEdge: ObligationEdge!
}
type DeleteControlObligationMappingPayload {
deletedControlId: ID!
deletedObligationId: ID!
}
type CreateControlSnapshotMappingPayload {
controlEdge: ControlEdge!
snapshotEdge: SnapshotEdge!
@@ -4656,6 +4894,18 @@ type DeleteMeetingPayload {
deletedMeetingId: ID!
}
type CreateStateOfApplicabilityPayload {
stateOfApplicabilityEdge: StateOfApplicabilityEdge!
}
type UpdateStateOfApplicabilityPayload {
stateOfApplicability: StateOfApplicability!
}
type DeleteStateOfApplicabilityPayload {
deletedStateOfApplicabilityId: ID!
}
type ConfirmEmailPayload {
success: Boolean!
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ type Control struct {
Description *string `json:"description,omitempty"`
Status coredata.ControlStatus `json:"status"`
ExclusionJustification *string `json:"exclusionJustification,omitempty"`
BestPractice bool `json:"bestPractice"`
Framework *Framework `json:"framework"`
Measures *MeasureConnection `json:"measures"`
Documents *DocumentConnection `json:"documents"`
@@ -72,6 +73,7 @@ func NewControl(control *coredata.Control) *Control {
Description: control.Description,
Status: control.Status,
ExclusionJustification: control.ExclusionJustification,
BestPractice: control.BestPractice,
CreatedAt: control.CreatedAt,
UpdatedAt: control.UpdatedAt,
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
StateOfApplicabilityOrderBy OrderBy[coredata.StateOfApplicabilityOrderField]
StateOfApplicabilityConnection struct {
TotalCount int
Edges []*StateOfApplicabilityEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
Filters *coredata.StateOfApplicabilityFilter
}
)
func NewStateOfApplicabilityConnection(
p *page.Page[*coredata.StateOfApplicability, coredata.StateOfApplicabilityOrderField],
parentType any,
parentID gid.GID,
filters *coredata.StateOfApplicabilityFilter,
) *StateOfApplicabilityConnection {
var edges = make([]*StateOfApplicabilityEdge, len(p.Data))
for i := range edges {
edges[i] = NewStateOfApplicabilityEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &StateOfApplicabilityConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: parentType,
ParentID: parentID,
Filters: filters,
}
}
func NewStateOfApplicabilityEdge(soa *coredata.StateOfApplicability, orderBy coredata.StateOfApplicabilityOrderField) *StateOfApplicabilityEdge {
return &StateOfApplicabilityEdge{
Cursor: soa.CursorKey(orderBy),
Node: NewStateOfApplicability(soa),
}
}
func NewStateOfApplicability(soa *coredata.StateOfApplicability) *StateOfApplicability {
return &StateOfApplicability{
ID: soa.ID,
Name: soa.Name,
SourceID: soa.SourceID,
SnapshotID: soa.SnapshotID,
CreatedAt: soa.CreatedAt,
UpdatedAt: soa.UpdatedAt,
}
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
StateOfApplicabilityControlConnection struct {
TotalCount int
Edges []*StateOfApplicabilityControlEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
StateOfApplicabilityControlEdge struct {
Cursor page.CursorKey
Node *StateOfApplicabilityControl
}
)
func NewStateOfApplicabilityControlConnection(
p *page.Page[*coredata.StateOfApplicabilityControl, coredata.StateOfApplicabilityOrderField],
parentType any,
parentID gid.GID,
) *StateOfApplicabilityControlConnection {
edges := make([]*StateOfApplicabilityControlEdge, len(p.Data))
for i, control := range p.Data {
edges[i] = NewStateOfApplicabilityControlEdge(control, p.Cursor.OrderBy.Field)
}
return &StateOfApplicabilityControlConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: parentType,
ParentID: parentID,
}
}
func NewStateOfApplicabilityControlEdge(
control *coredata.StateOfApplicabilityControl,
orderBy coredata.StateOfApplicabilityOrderField,
) *StateOfApplicabilityControlEdge {
return &StateOfApplicabilityControlEdge{
Cursor: control.CursorKey(orderBy),
Node: NewStateOfApplicabilityControl(control),
}
}
func NewStateOfApplicabilityControl(control *coredata.StateOfApplicabilityControl) *StateOfApplicabilityControl {
return &StateOfApplicabilityControl{
ID: control.ID,
StateOfApplicabilityID: control.StateOfApplicabilityID,
ControlID: control.ControlID,
Applicability: control.Applicability,
Justification: control.Justification,
}
}

View File

@@ -88,6 +88,22 @@ type AuditEdge struct {
Node *Audit `json:"node"`
}
type AvailableStateOfApplicabilityControl struct {
ControlID gid.GID `json:"controlId"`
SectionTitle string `json:"sectionTitle"`
Name string `json:"name"`
FrameworkID gid.GID `json:"frameworkId"`
FrameworkName string `json:"frameworkName"`
OrganizationID gid.GID `json:"organizationId"`
StateOfApplicabilityID *gid.GID `json:"stateOfApplicabilityId,omitempty"`
Applicability *bool `json:"applicability,omitempty"`
Justification *string `json:"justification,omitempty"`
BestPractice bool `json:"bestPractice"`
Regulatory bool `json:"regulatory"`
Contractual bool `json:"contractual"`
RiskAssessment bool `json:"riskAssessment"`
}
type BulkDeleteDocumentsInput struct {
DocumentIds []gid.GID `json:"documentIds"`
}
@@ -249,6 +265,7 @@ type CreateControlInput struct {
Description *string `json:"description,omitempty"`
Status coredata.ControlStatus `json:"status"`
ExclusionJustification *string `json:"exclusionJustification,omitempty"`
BestPractice bool `json:"bestPractice"`
}
type CreateControlMeasureMappingInput struct {
@@ -261,6 +278,16 @@ type CreateControlMeasureMappingPayload struct {
MeasureEdge *MeasureEdge `json:"measureEdge"`
}
type CreateControlObligationMappingInput struct {
ControlID gid.GID `json:"controlId"`
ObligationID gid.GID `json:"obligationId"`
}
type CreateControlObligationMappingPayload struct {
ControlEdge *ControlEdge `json:"controlEdge"`
ObligationEdge *ObligationEdge `json:"obligationEdge"`
}
type CreateControlPayload struct {
ControlEdge *ControlEdge `json:"controlEdge"`
}
@@ -407,6 +434,7 @@ type CreateObligationInput struct {
LastReviewDate *time.Time `json:"lastReviewDate,omitempty"`
DueDate *time.Time `json:"dueDate,omitempty"`
Status coredata.ObligationStatus `json:"status"`
Type coredata.ObligationType `json:"type"`
}
type CreateObligationPayload struct {
@@ -560,6 +588,27 @@ type CreateSnapshotPayload struct {
SnapshotEdge *SnapshotEdge `json:"snapshotEdge"`
}
type CreateStateOfApplicabilityControlMappingInput struct {
StateOfApplicabilityID gid.GID `json:"stateOfApplicabilityId"`
ControlID gid.GID `json:"controlId"`
Applicability bool `json:"applicability"`
Justification *string `json:"justification,omitempty"`
}
type CreateStateOfApplicabilityControlMappingPayload struct {
StateOfApplicabilityControlEdge *StateOfApplicabilityControlEdge `json:"stateOfApplicabilityControlEdge"`
}
type CreateStateOfApplicabilityInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
OwnerID gid.GID `json:"ownerId"`
}
type CreateStateOfApplicabilityPayload struct {
StateOfApplicabilityEdge *StateOfApplicabilityEdge `json:"stateOfApplicabilityEdge"`
}
type CreateTaskInput struct {
OrganizationID gid.GID `json:"organizationId"`
MeasureID *gid.GID `json:"measureId,omitempty"`
@@ -807,6 +856,16 @@ type DeleteControlMeasureMappingPayload struct {
DeletedMeasureID gid.GID `json:"deletedMeasureId"`
}
type DeleteControlObligationMappingInput struct {
ControlID gid.GID `json:"controlId"`
ObligationID gid.GID `json:"obligationId"`
}
type DeleteControlObligationMappingPayload struct {
DeletedControlID gid.GID `json:"deletedControlId"`
DeletedObligationID gid.GID `json:"deletedObligationId"`
}
type DeleteControlPayload struct {
DeletedControlID gid.GID `json:"deletedControlId"`
}
@@ -1011,6 +1070,25 @@ type DeleteSnapshotPayload struct {
DeletedSnapshotID gid.GID `json:"deletedSnapshotId"`
}
type DeleteStateOfApplicabilityControlMappingInput struct {
StateOfApplicabilityID gid.GID `json:"stateOfApplicabilityId"`
ControlID gid.GID `json:"controlId"`
}
type DeleteStateOfApplicabilityControlMappingPayload struct {
DeletedStateOfApplicabilityID gid.GID `json:"deletedStateOfApplicabilityId"`
DeletedControlID gid.GID `json:"deletedControlId"`
DeletedStateOfApplicabilityControlID gid.GID `json:"deletedStateOfApplicabilityControlId"`
}
type DeleteStateOfApplicabilityInput struct {
StateOfApplicabilityID gid.GID `json:"stateOfApplicabilityId"`
}
type DeleteStateOfApplicabilityPayload struct {
DeletedStateOfApplicabilityID gid.GID `json:"deletedStateOfApplicabilityId"`
}
type DeleteTaskInput struct {
TaskID gid.GID `json:"taskId"`
}
@@ -1265,6 +1343,14 @@ type ExportSignableDocumentVersionPDFPayload struct {
Data string `json:"data"`
}
type ExportStateOfApplicabilityPDFInput struct {
StateOfApplicabilityID gid.GID `json:"stateOfApplicabilityId"`
}
type ExportStateOfApplicabilityPDFPayload struct {
Data string `json:"data"`
}
type ExportTransferImpactAssessmentsPDFInput struct {
OrganizationID gid.GID `json:"organizationId"`
Filter *TransferImpactAssessmentFilter `json:"filter,omitempty"`
@@ -1521,6 +1607,7 @@ type Obligation struct {
LastReviewDate *time.Time `json:"lastReviewDate,omitempty"`
DueDate *time.Time `json:"dueDate,omitempty"`
Status coredata.ObligationStatus `json:"status"`
Type coredata.ObligationType `json:"type"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
@@ -1556,6 +1643,7 @@ type Organization struct {
Peoples *PeopleConnection `json:"peoples"`
Documents *DocumentConnection `json:"documents"`
Meetings *MeetingConnection `json:"meetings"`
StatesOfApplicability *StateOfApplicabilityConnection `json:"statesOfApplicability"`
Measures *MeasureConnection `json:"measures"`
Risks *RiskConnection `json:"risks"`
Tasks *TaskConnection `json:"tasks"`
@@ -1878,6 +1966,46 @@ type SnapshotEdge struct {
Node *Snapshot `json:"node"`
}
type StateOfApplicability struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
SourceID *gid.GID `json:"sourceId,omitempty"`
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
Organization *Organization `json:"organization,omitempty"`
Owner *People `json:"owner"`
Controls *ControlConnection `json:"controls"`
AvailableControls []*AvailableStateOfApplicabilityControl `json:"availableControls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (StateOfApplicability) IsNode() {}
func (this StateOfApplicability) GetID() gid.GID { return this.ID }
type StateOfApplicabilityControl struct {
ID gid.GID `json:"id"`
StateOfApplicabilityID gid.GID `json:"stateOfApplicabilityId"`
ControlID gid.GID `json:"controlId"`
StateOfApplicability *StateOfApplicability `json:"stateOfApplicability"`
Applicability bool `json:"applicability"`
Justification *string `json:"justification,omitempty"`
}
type StateOfApplicabilityControlInput struct {
ControlID gid.GID `json:"controlId"`
Applicability bool `json:"applicability"`
Justification *string `json:"justification,omitempty"`
}
type StateOfApplicabilityEdge struct {
Cursor page.CursorKey `json:"cursor"`
Node *StateOfApplicability `json:"node"`
}
type StateOfApplicabilityFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
type Task struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
@@ -2072,6 +2200,7 @@ type UpdateControlInput struct {
Description graphql.Omittable[*string] `json:"description,omitempty"`
Status *coredata.ControlStatus `json:"status,omitempty"`
ExclusionJustification *string `json:"exclusionJustification,omitempty"`
BestPractice *bool `json:"bestPractice,omitempty"`
}
type UpdateControlPayload struct {
@@ -2199,6 +2328,7 @@ type UpdateObligationInput struct {
LastReviewDate graphql.Omittable[*time.Time] `json:"lastReviewDate,omitempty"`
DueDate graphql.Omittable[*time.Time] `json:"dueDate,omitempty"`
Status *coredata.ObligationStatus `json:"status,omitempty"`
Type *coredata.ObligationType `json:"type,omitempty"`
}
type UpdateObligationPayload struct {
@@ -2326,6 +2456,16 @@ type UpdateSAMLConfigurationPayload struct {
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
}
type UpdateStateOfApplicabilityInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
OwnerID *gid.GID `json:"ownerId,omitempty"`
}
type UpdateStateOfApplicabilityPayload struct {
StateOfApplicability *StateOfApplicability `json:"stateOfApplicability"`
}
type UpdateTaskInput struct {
TaskID gid.GID `json:"taskId"`
Name *string `json:"name,omitempty"`

View File

@@ -482,6 +482,38 @@ func (r *controlResolver) Audits(ctx context.Context, obj *types.Control, first
return types.NewAuditConnection(page, r, obj.ID), nil
}
// Obligations is the resolver for the obligations field.
func (r *controlResolver) Obligations(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy, filter *types.ObligationFilter) (*types.ObligationConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListObligations)
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.ObligationOrderField]{
Field: coredata.ObligationOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.ObligationOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var snapshotID **gid.GID
if filter != nil {
snapshotID = &filter.SnapshotID
}
obligationFilter := coredata.NewObligationFilter(snapshotID)
page, err := prb.Obligations.ListForControlID(ctx, obj.ID, cursor, obligationFilter)
if err != nil {
panic(fmt.Errorf("cannot list control obligations: %w", err))
}
return types.NewObligationConnection(page, r, obj.ID, filter), nil
}
// Snapshots is the resolver for the snapshots field.
func (r *controlResolver) Snapshots(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) (*types.SnapshotConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListSnapshots)
@@ -510,6 +542,33 @@ func (r *controlResolver) Snapshots(ctx context.Context, obj *types.Control, fir
return types.NewSnapshotConnection(page, r, obj.ID), nil
}
// StateOfApplicabilityControls is the resolver for the stateOfApplicabilityControls field.
func (r *controlResolver) StateOfApplicabilityControls(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.StateOfApplicabilityOrderBy) (*types.StateOfApplicabilityControlConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListStatesOfApplicability)
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.StateOfApplicabilityOrderField]{
Field: coredata.StateOfApplicabilityOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.StateOfApplicabilityOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
p, err := prb.StatesOfApplicability.ListControlLinks(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list state of applicability controls: %w", err))
}
return types.NewStateOfApplicabilityControlConnection(p, r, obj.ID), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.ControlConnection) (int, error) {
prb := r.ProboService(ctx, obj.ParentID.TenantID())
@@ -546,6 +605,12 @@ func (r *controlConnectionResolver) TotalCount(ctx context.Context, obj *types.C
panic(fmt.Errorf("cannot count controls: %w", err))
}
return count, nil
case *stateOfApplicabilityResolver:
count, err := prb.Controls.CountForStateOfApplicabilityID(ctx, obj.ParentID, obj.Filters)
if err != nil {
panic(fmt.Errorf("cannot count controls: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
@@ -2502,6 +2567,7 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create
SectionTitle: input.SectionTitle,
Status: &input.Status,
ExclusionJustification: input.ExclusionJustification,
BestPractice: input.BestPractice,
})
if err != nil {
var errAlreadyExists *coredata.ErrControlAlreadyExists
@@ -2529,6 +2595,7 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
SectionTitle: input.SectionTitle,
Status: input.Status,
ExclusionJustification: input.ExclusionJustification,
BestPractice: input.BestPractice,
})
if err != nil {
@@ -2718,6 +2785,40 @@ func (r *mutationResolver) DeleteControlDocumentMapping(ctx context.Context, inp
}, nil
}
// CreateStateOfApplicabilityControlMapping is the resolver for the createStateOfApplicabilityControlMapping field.
func (r *mutationResolver) CreateStateOfApplicabilityControlMapping(ctx context.Context, input types.CreateStateOfApplicabilityControlMappingInput) (*types.CreateStateOfApplicabilityControlMappingPayload, error) {
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateStateOfApplicabilityControlMapping)
prb := r.ProboService(ctx, input.StateOfApplicabilityID.TenantID())
control, err := prb.StatesOfApplicability.LinkControl(ctx, input.StateOfApplicabilityID, input.ControlID, input.Applicability, input.Justification)
if err != nil {
panic(fmt.Errorf("cannot create state of applicability control mapping: %w", err))
}
return &types.CreateStateOfApplicabilityControlMappingPayload{
StateOfApplicabilityControlEdge: types.NewStateOfApplicabilityControlEdge(control, coredata.StateOfApplicabilityOrderFieldCreatedAt),
}, nil
}
// DeleteStateOfApplicabilityControlMapping is the resolver for the deleteStateOfApplicabilityControlMapping field.
func (r *mutationResolver) DeleteStateOfApplicabilityControlMapping(ctx context.Context, input types.DeleteStateOfApplicabilityControlMappingInput) (*types.DeleteStateOfApplicabilityControlMappingPayload, error) {
r.MustBeAuthorized(ctx, input.StateOfApplicabilityID, authz.ActionDeleteStateOfApplicabilityControlMapping)
prb := r.ProboService(ctx, input.StateOfApplicabilityID.TenantID())
deletedID, err := prb.StatesOfApplicability.DeleteControlLink(ctx, input.StateOfApplicabilityID, input.ControlID)
if err != nil {
panic(fmt.Errorf("cannot delete state of applicability control mapping: %w", err))
}
return &types.DeleteStateOfApplicabilityControlMappingPayload{
DeletedStateOfApplicabilityID: input.StateOfApplicabilityID,
DeletedControlID: input.ControlID,
DeletedStateOfApplicabilityControlID: deletedID,
}, nil
}
// CreateControlAuditMapping is the resolver for the createControlAuditMapping field.
func (r *mutationResolver) CreateControlAuditMapping(ctx context.Context, input types.CreateControlAuditMappingInput) (*types.CreateControlAuditMappingPayload, error) {
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlAuditMapping)
@@ -2752,6 +2853,40 @@ func (r *mutationResolver) DeleteControlAuditMapping(ctx context.Context, input
}, nil
}
// CreateControlObligationMapping is the resolver for the createControlObligationMapping field.
func (r *mutationResolver) CreateControlObligationMapping(ctx context.Context, input types.CreateControlObligationMappingInput) (*types.CreateControlObligationMappingPayload, error) {
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlObligationMapping)
prb := r.ProboService(ctx, input.ObligationID.TenantID())
control, obligation, err := prb.Controls.CreateObligationMapping(ctx, input.ControlID, input.ObligationID)
if err != nil {
panic(fmt.Errorf("cannot create control obligation mapping: %w", err))
}
return &types.CreateControlObligationMappingPayload{
ControlEdge: types.NewControlEdge(control, coredata.ControlOrderFieldCreatedAt),
ObligationEdge: types.NewObligationEdge(obligation, coredata.ObligationOrderFieldCreatedAt),
}, nil
}
// DeleteControlObligationMapping is the resolver for the deleteControlObligationMapping field.
func (r *mutationResolver) DeleteControlObligationMapping(ctx context.Context, input types.DeleteControlObligationMappingInput) (*types.DeleteControlObligationMappingPayload, error) {
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionDeleteControlObligationMapping)
prb := r.ProboService(ctx, input.ObligationID.TenantID())
control, obligation, err := prb.Controls.DeleteObligationMapping(ctx, input.ControlID, input.ObligationID)
if err != nil {
panic(fmt.Errorf("cannot delete control obligation mapping: %w", err))
}
return &types.DeleteControlObligationMappingPayload{
DeletedControlID: control.ID,
DeletedObligationID: obligation.ID,
}, nil
}
// CreateControlSnapshotMapping is the resolver for the createControlSnapshotMapping field.
func (r *mutationResolver) CreateControlSnapshotMapping(ctx context.Context, input types.CreateControlSnapshotMappingInput) (*types.CreateControlSnapshotMappingPayload, error) {
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlSnapshotMapping)
@@ -3396,6 +3531,100 @@ func (r *mutationResolver) DeleteMeeting(ctx context.Context, input types.Delete
}, nil
}
// CreateStateOfApplicability is the resolver for the createStateOfApplicability field.
func (r *mutationResolver) CreateStateOfApplicability(ctx context.Context, input types.CreateStateOfApplicabilityInput) (*types.CreateStateOfApplicabilityPayload, error) {
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateStateOfApplicability)
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
stateOfApplicability, err := prb.StatesOfApplicability.Create(
ctx,
probo.CreateStateOfApplicabilityRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
OwnerID: input.OwnerID,
},
)
if err != nil {
var errAlreadyExists *coredata.ErrStateOfApplicabilityAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot create state_of_applicability: %w", err))
}
return &types.CreateStateOfApplicabilityPayload{
StateOfApplicabilityEdge: types.NewStateOfApplicabilityEdge(stateOfApplicability, coredata.StateOfApplicabilityOrderFieldCreatedAt),
}, nil
}
// UpdateStateOfApplicability is the resolver for the updateStateOfApplicability field.
func (r *mutationResolver) UpdateStateOfApplicability(ctx context.Context, input types.UpdateStateOfApplicabilityInput) (*types.UpdateStateOfApplicabilityPayload, error) {
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateStateOfApplicability)
prb := r.ProboService(ctx, input.ID.TenantID())
var name *string
if input.Name != nil {
name = input.Name
}
stateOfApplicability, err := prb.StatesOfApplicability.Update(
ctx,
probo.UpdateStateOfApplicabilityRequest{
StateOfApplicabilityID: input.ID,
Name: name,
OwnerID: input.OwnerID,
},
)
if err != nil {
var errAlreadyExists *coredata.ErrStateOfApplicabilityAlreadyExists
if errors.As(err, &errAlreadyExists) {
return nil, gqlutils.Conflict(errAlreadyExists)
}
panic(fmt.Errorf("cannot update state_of_applicability: %w", err))
}
return &types.UpdateStateOfApplicabilityPayload{
StateOfApplicability: types.NewStateOfApplicability(stateOfApplicability),
}, nil
}
// DeleteStateOfApplicability is the resolver for the deleteStateOfApplicability field.
func (r *mutationResolver) DeleteStateOfApplicability(ctx context.Context, input types.DeleteStateOfApplicabilityInput) (*types.DeleteStateOfApplicabilityPayload, error) {
r.MustBeAuthorized(ctx, input.StateOfApplicabilityID, authz.ActionDeleteStateOfApplicability)
prb := r.ProboService(ctx, input.StateOfApplicabilityID.TenantID())
err := prb.StatesOfApplicability.Delete(ctx, input.StateOfApplicabilityID)
if err != nil {
panic(fmt.Errorf("cannot delete state_of_applicability: %w", err))
}
return &types.DeleteStateOfApplicabilityPayload{
DeletedStateOfApplicabilityID: input.StateOfApplicabilityID,
}, nil
}
// ExportStateOfApplicabilityPDF is the resolver for the exportStateOfApplicabilityPDF field.
func (r *mutationResolver) ExportStateOfApplicabilityPDF(ctx context.Context, input types.ExportStateOfApplicabilityPDFInput) (*types.ExportStateOfApplicabilityPDFPayload, error) {
r.MustBeAuthorized(ctx, input.StateOfApplicabilityID, authz.ActionUpdateStateOfApplicability)
prb := r.ProboService(ctx, input.StateOfApplicabilityID.TenantID())
pdfData, err := prb.StatesOfApplicability.ExportPDF(ctx, input.StateOfApplicabilityID)
if err != nil {
panic(fmt.Errorf("cannot export state of applicability PDF: %w", err))
}
base64Data := base64.StdEncoding.EncodeToString(pdfData)
dataURI := fmt.Sprintf("data:application/pdf;base64,%s", base64Data)
return &types.ExportStateOfApplicabilityPDFPayload{
Data: dataURI,
}, nil
}
// PublishDocumentVersion is the resolver for the publishDocumentVersion field.
func (r *mutationResolver) PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error) {
r.MustBeAuthorized(ctx, input.DocumentID, authz.ActionPublishDocumentVersion)
@@ -4194,7 +4423,8 @@ func (r *mutationResolver) CreateObligation(ctx context.Context, input types.Cre
OwnerID: input.OwnerID,
LastReviewDate: input.LastReviewDate,
DueDate: input.DueDate,
Status: &input.Status,
Status: input.Status,
Type: input.Type,
}
obligation, err := prb.Obligations.Create(ctx, &req)
@@ -4224,6 +4454,7 @@ func (r *mutationResolver) UpdateObligation(ctx context.Context, input types.Upd
LastReviewDate: UnwrapOmittable(input.LastReviewDate),
DueDate: UnwrapOmittable(input.DueDate),
Status: input.Status,
Type: input.Type,
}
obligation, err := prb.Obligations.Update(ctx, &req)
@@ -5403,6 +5634,38 @@ func (r *organizationResolver) Meetings(ctx context.Context, obj *types.Organiza
return types.NewMeetingConnection(page, r, obj.ID), nil
}
// StatesOfApplicability is the resolver for the statesOfApplicability field.
func (r *organizationResolver) StatesOfApplicability(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.StateOfApplicabilityOrderBy, filter *types.StateOfApplicabilityFilter) (*types.StateOfApplicabilityConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListStatesOfApplicability)
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.StateOfApplicabilityOrderField]{
Field: coredata.StateOfApplicabilityOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.StateOfApplicabilityOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var stateOfApplicabilityFilter = coredata.NewStateOfApplicabilityFilter(nil)
if filter != nil {
stateOfApplicabilityFilter = coredata.NewStateOfApplicabilityFilter(&filter.SnapshotID)
}
page, err := prb.StatesOfApplicability.ListForOrganizationID(ctx, obj.ID, cursor, stateOfApplicabilityFilter)
if err != nil {
panic(fmt.Errorf("cannot list organization states_of_applicability: %w", err))
}
return types.NewStateOfApplicabilityConnection(page, r, obj.ID, stateOfApplicabilityFilter), nil
}
// Measures is the resolver for the measures field.
func (r *organizationResolver) Measures(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MeasureOrderBy, filter *types.MeasureFilter) (*types.MeasureConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListMeasures)
@@ -6334,6 +6597,17 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
}
return types.NewRightsRequest(rightsRequest), nil
case coredata.StateOfApplicabilityEntityType:
stateOfApplicability, err := prb.StatesOfApplicability.Get(ctx, id)
if err != nil {
var errNotFound *coredata.ErrStateOfApplicabilityNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot get state_of_applicability: %w", err))
}
return types.NewStateOfApplicability(stateOfApplicability), nil
default:
}
@@ -6804,6 +7078,151 @@ func (r *snapshotConnectionResolver) TotalCount(ctx context.Context, obj *types.
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// Organization is the resolver for the organization field.
func (r *stateOfApplicabilityResolver) Organization(ctx context.Context, obj *types.StateOfApplicability) (*types.Organization, error) {
prb := r.ProboService(ctx, obj.ID.TenantID())
stateOfApplicability, err := prb.StatesOfApplicability.Get(ctx, obj.ID)
if err != nil {
var errNotFound *coredata.ErrStateOfApplicabilityNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load state_of_applicability: %w", err))
}
organization, err := prb.Organizations.Get(ctx, stateOfApplicability.OrganizationID)
if err != nil {
var errNotFound *coredata.ErrOrganizationNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load organization: %w", err))
}
return types.NewOrganization(organization), nil
}
// Owner is the resolver for the owner field.
func (r *stateOfApplicabilityResolver) Owner(ctx context.Context, obj *types.StateOfApplicability) (*types.People, error) {
prb := r.ProboService(ctx, obj.ID.TenantID())
stateOfApplicability, err := prb.StatesOfApplicability.Get(ctx, obj.ID)
if err != nil {
var errNotFound *coredata.ErrStateOfApplicabilityNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load state_of_applicability: %w", err))
}
people, err := prb.Peoples.Get(ctx, stateOfApplicability.OwnerID)
if err != nil {
var errNotFound *coredata.ErrPeopleNotFound
if errors.As(err, &errNotFound) {
return nil, gqlutils.NotFound(errNotFound)
}
panic(fmt.Errorf("cannot load owner: %w", err))
}
return types.NewPeople(people), nil
}
// Controls is the resolver for the controls field.
func (r *stateOfApplicabilityResolver) Controls(ctx context.Context, obj *types.StateOfApplicability, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListControls)
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var controlFilter = coredata.NewControlFilter(nil)
if filter != nil {
controlFilter = coredata.NewControlFilter(filter.Query)
}
page, err := prb.Controls.ListForStateOfApplicabilityID(ctx, obj.ID, cursor, controlFilter)
if err != nil {
panic(fmt.Errorf("cannot list state of applicability controls: %w", err))
}
return types.NewControlConnection(page, r, obj.ID, controlFilter), nil
}
// AvailableControls is the resolver for the availableControls field.
func (r *stateOfApplicabilityResolver) AvailableControls(ctx context.Context, obj *types.StateOfApplicability) ([]*types.AvailableStateOfApplicabilityControl, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionListControls)
prb := r.ProboService(ctx, obj.ID.TenantID())
availableControls, err := prb.StatesOfApplicability.ListAvailableControls(ctx, obj.ID)
if err != nil {
panic(fmt.Errorf("cannot list available controls: %w", err))
}
result := make([]*types.AvailableStateOfApplicabilityControl, 0, len(availableControls))
for _, ac := range availableControls {
result = append(result, &types.AvailableStateOfApplicabilityControl{
ControlID: ac.ControlID,
SectionTitle: ac.SectionTitle,
Name: ac.Name,
FrameworkID: ac.FrameworkID,
FrameworkName: ac.FrameworkName,
OrganizationID: ac.OrganizationID,
StateOfApplicabilityID: ac.StateOfApplicabilityID,
Applicability: ac.Applicability,
Justification: ac.Justification,
BestPractice: ac.BestPractice,
Regulatory: ac.Regulatory,
Contractual: ac.Contractual,
RiskAssessment: ac.RiskAssessment,
})
}
return result, nil
}
// TotalCount is the resolver for the totalCount field.
func (r *stateOfApplicabilityConnectionResolver) TotalCount(ctx context.Context, obj *types.StateOfApplicabilityConnection) (int, error) {
prb := r.ProboService(ctx, obj.ParentID.TenantID())
switch obj.Resolver.(type) {
case *organizationResolver:
count, err := prb.StatesOfApplicability.CountForOrganizationID(ctx, obj.ParentID, obj.Filters)
if err != nil {
panic(fmt.Errorf("cannot count states_of_applicability: %w", err))
}
return count, nil
}
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
// StateOfApplicability is the resolver for the stateOfApplicability field.
func (r *stateOfApplicabilityControlResolver) StateOfApplicability(ctx context.Context, obj *types.StateOfApplicabilityControl) (*types.StateOfApplicability, error) {
r.MustBeAuthorized(ctx, obj.StateOfApplicabilityID, authz.ActionGet)
prb := r.ProboService(ctx, obj.StateOfApplicabilityID.TenantID())
soa, err := prb.StatesOfApplicability.Get(ctx, obj.StateOfApplicabilityID)
if err != nil {
panic(fmt.Errorf("cannot get state of applicability: %w", err))
}
return types.NewStateOfApplicability(soa), nil
}
// AssignedTo is the resolver for the assignedTo field.
func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.People, error) {
r.MustBeAuthorized(ctx, obj.ID, authz.ActionGetAssignedTo)
@@ -8055,6 +8474,21 @@ func (r *Resolver) SnapshotConnection() schema.SnapshotConnectionResolver {
return &snapshotConnectionResolver{r}
}
// StateOfApplicability returns schema.StateOfApplicabilityResolver implementation.
func (r *Resolver) StateOfApplicability() schema.StateOfApplicabilityResolver {
return &stateOfApplicabilityResolver{r}
}
// StateOfApplicabilityConnection returns schema.StateOfApplicabilityConnectionResolver implementation.
func (r *Resolver) StateOfApplicabilityConnection() schema.StateOfApplicabilityConnectionResolver {
return &stateOfApplicabilityConnectionResolver{r}
}
// StateOfApplicabilityControl returns schema.StateOfApplicabilityControlResolver implementation.
func (r *Resolver) StateOfApplicabilityControl() schema.StateOfApplicabilityControlResolver {
return &stateOfApplicabilityControlResolver{r}
}
// Task returns schema.TaskResolver implementation.
func (r *Resolver) Task() schema.TaskResolver { return &taskResolver{r} }
@@ -8197,6 +8631,9 @@ type sAMLConfigurationResolver struct{ *Resolver }
type signableDocumentResolver struct{ *Resolver }
type snapshotResolver struct{ *Resolver }
type snapshotConnectionResolver struct{ *Resolver }
type stateOfApplicabilityResolver struct{ *Resolver }
type stateOfApplicabilityConnectionResolver struct{ *Resolver }
type stateOfApplicabilityControlResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskConnectionResolver struct{ *Resolver }
type transferImpactAssessmentResolver struct{ *Resolver }

View File

@@ -868,7 +868,8 @@ func (r *Resolver) AddObligationTool(ctx context.Context, req *mcp.CallToolReque
OwnerID: input.OwnerID,
LastReviewDate: input.LastReviewDate,
DueDate: input.DueDate,
Status: input.Status,
Status: *input.Status,
Type: *input.Type,
},
)
if err != nil {
@@ -898,6 +899,7 @@ func (r *Resolver) UpdateObligationTool(ctx context.Context, req *mcp.CallToolRe
LastReviewDate: UnwrapOmittable(input.LastReviewDate),
DueDate: UnwrapOmittable(input.DueDate),
Status: input.Status,
Type: input.Type,
},
)
if err != nil {

View File

@@ -1759,6 +1759,13 @@ components:
- COMPLIANT
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ObligationStatus
ObligationType:
type: string
enum:
- LEGAL
- CONTRACTUAL
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ObligationType
ObligationOrderField:
type: string
enum:
@@ -1921,6 +1928,7 @@ components:
- organization_id
- owner_id
- status
- type
properties:
organization_id:
$ref: "#/components/schemas/GID"
@@ -1958,6 +1966,13 @@ components:
- type: "null"
description: No status
description: Status
type:
anyOf:
- $ref: "#/components/schemas/ObligationType"
description: Obligation type
- type: "null"
description: No type
description: Type
AddObligationOutput:
type: object
@@ -2018,6 +2033,13 @@ components:
- type: "null"
description: No status
description: Status
type:
anyOf:
- $ref: "#/components/schemas/ObligationType"
description: Obligation type
- type: "null"
description: No type
description: Type
UpdateObligationOutput:
type: object
@@ -3150,6 +3172,7 @@ components:
- OBLIGATIONS
- CONTINUAL_IMPROVEMENTS
- PROCESSING_ACTIVITIES
- STATES_OF_APPLICABILITY
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SnapshotsType
SnapshotOrderField:

View File

@@ -31,7 +31,7 @@ var (
AddMeasureToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["measure"],"properties":{"measure":{"type":"object","required":["id","category","name","state","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Measure category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Measure description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Measure name"},"state":{"type":"string","enum":["NOT_STARTED","IN_PROGRESS","NOT_APPLICABLE","IMPLEMENTED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
AddNonconformityToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","reference_id","root_cause","owner_id","status"],"properties":{"audit_id":{"description":"Audit ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No audit"}]},"corrective_action":{"type":"string","description":"Corrective action"},"date_identified":{"type":"string","description":"Date identified","format":"date-time"},"description":{"type":"string","description":"Description"},"due_date":{"type":"string","description":"Due date","format":"date-time"},"effectiveness_check":{"type":"string","description":"Effectiveness check"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"reference_id":{"type":"string","description":"Reference ID"},"root_cause":{"type":"string","description":"Root cause"},"status":{"description":"Status","anyOf":[{"type":"string","enum":["OPEN","IN_PROGRESS","CLOSED"]},{"type":"null","description":"No status"}]}}}`)
AddNonconformityToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["nonconformity"],"properties":{"nonconformity":{"type":"object","required":["id","organization_id","reference_id","root_cause","owner_id","status","created_at","updated_at"],"properties":{"audit_id":{"description":"Audit ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No audit"}]},"corrective_action":{"description":"Corrective action"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"date_identified":{"description":"Date identified","format":"date-time"},"description":{"description":"Description"},"due_date":{"description":"Due date","format":"date-time"},"effectiveness_check":{"description":"Effectiveness check"},"id":{"type":"string","format":"string"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"reference_id":{"type":"string","description":"Reference ID"},"root_cause":{"type":"string","description":"Root cause"},"snapshot_id":{"description":"Snapshot ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No snapshot"}]},"status":{"type":"string","enum":["OPEN","IN_PROGRESS","CLOSED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
AddObligationToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","owner_id","status"],"properties":{"actions_to_be_implemented":{"type":"string","description":"Actions to be implemented"},"area":{"type":"string","description":"Area"},"due_date":{"type":"string","description":"Due date","format":"date-time"},"last_review_date":{"type":"string","description":"Last review date","format":"date-time"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"regulator":{"type":"string","description":"Regulator"},"requirement":{"type":"string","description":"Requirement"},"source":{"type":"string","description":"Source"},"status":{"description":"Status","anyOf":[{"type":"string","enum":["NON_COMPLIANT","PARTIALLY_COMPLIANT","COMPLIANT"]},{"type":"null","description":"No status"}]}}}`)
AddObligationToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","owner_id","status","type"],"properties":{"actions_to_be_implemented":{"type":"string","description":"Actions to be implemented"},"area":{"type":"string","description":"Area"},"due_date":{"type":"string","description":"Due date","format":"date-time"},"last_review_date":{"type":"string","description":"Last review date","format":"date-time"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"regulator":{"type":"string","description":"Regulator"},"requirement":{"type":"string","description":"Requirement"},"source":{"type":"string","description":"Source"},"status":{"description":"Status","anyOf":[{"type":"string","enum":["NON_COMPLIANT","PARTIALLY_COMPLIANT","COMPLIANT"]},{"type":"null","description":"No status"}]},"type":{"description":"Type","anyOf":[{"type":"string","enum":["LEGAL","CONTRACTUAL"]},{"type":"null","description":"No type"}]}}}`)
AddObligationToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["obligation"],"properties":{"obligation":{"type":"object","required":["id","organization_id","owner_id","status","created_at","updated_at"],"properties":{"actions_to_be_implemented":{"description":"Actions to be implemented"},"area":{"description":"Area"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"due_date":{"description":"Due date","format":"date-time"},"id":{"type":"string","format":"string"},"last_review_date":{"description":"Last review date","format":"date-time"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"regulator":{"description":"Regulator"},"requirement":{"description":"Requirement"},"snapshot_id":{"description":"Snapshot ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No snapshot"}]},"source":{"description":"Source"},"source_id":{"description":"Source ID"},"status":{"type":"string","enum":["NON_COMPLIANT","PARTIALLY_COMPLIANT","COMPLIANT"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
AddPeopleToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","full_name","primary_email_address","kind"],"properties":{"additional_email_addresses":{"type":"array","description":"Additional email addresses","items":{"type":"string","format":"string"}},"contract_end_date":{"type":"string","description":"Contract end date","format":"date-time"},"contract_start_date":{"type":"string","description":"Contract start date","format":"date-time"},"full_name":{"type":"string","description":"Full name"},"kind":{"type":"string","enum":["EMPLOYEE","CONTRACTOR","SERVICE_ACCOUNT"]},"organization_id":{"type":"string","format":"string"},"position":{"type":"string","description":"Position"},"primary_email_address":{"type":"string","format":"string"}}}`)
AddPeopleToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["people"],"properties":{"people":{"type":"object","required":["id","organization_id","full_name","primary_email_address","additional_email_addresses","kind","created_at","updated_at"],"properties":{"additional_email_addresses":{"type":"array","description":"Additional email addresses","items":{"type":"string","format":"string"}},"contract_end_date":{"description":"Contract end date","format":"date-time"},"contract_start_date":{"description":"Contract start date","format":"date-time"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"full_name":{"type":"string","description":"Full name"},"id":{"type":"string","format":"string"},"kind":{"type":"string","enum":["EMPLOYEE","CONTRACTOR","SERVICE_ACCOUNT"]},"organization_id":{"type":"string","format":"string"},"position":{"description":"Position"},"primary_email_address":{"type":"string","format":"string"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
@@ -80,7 +80,7 @@ var (
GetRiskToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"string"}}}`)
GetRiskToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["risk"],"properties":{"risk":{"type":"object","required":["id","organization_id","name","category","treatment","inherent_likelihood","inherent_impact","inherent_risk_score","residual_likelihood","residual_impact","residual_risk_score","note","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Risk category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"inherent_risk_score":{"type":"integer","description":"Inherent risk score"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No owner"}]},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"residual_risk_score":{"type":"integer","description":"Residual risk score"},"snapshot_id":{"description":"Snapshot ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No snapshot"}]},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
GetSnapshotToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"string"}}}`)
GetSnapshotToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["snapshot"],"properties":{"snapshot":{"type":"object","required":["id","organization_id","name","type","created_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Snapshot description","anyOf":[{"type":"string","description":"Snapshot description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES"]}}}}}`)
GetSnapshotToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["snapshot"],"properties":{"snapshot":{"type":"object","required":["id","organization_id","name","type","created_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Snapshot description","anyOf":[{"type":"string","description":"Snapshot description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES","STATES_OF_APPLICABILITY"]}}}}}`)
GetTaskToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"string"}}}`)
GetTaskToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["task"],"properties":{"task":{"type":"object","required":["id","organization_id","name","state","created_at","updated_at"],"properties":{"assigned_to_id":{"description":"Assigned to person ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"Not assigned"}]},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"deadline":{"description":"Deadline","anyOf":[{"type":"string","description":"Deadline","format":"date-time"},{"type":"null","description":"No deadline"}]},"description":{"description":"Task description","anyOf":[{"type":"string","description":"Task description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"measure_id":{"description":"Measure ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No measure"}]},"name":{"type":"string","description":"Task name"},"organization_id":{"type":"string","format":"string"},"state":{"type":"string","enum":["TODO","DONE"]},"time_estimate":{"description":"Time estimate","anyOf":[{"type":"string","description":"A duration"},{"type":"null","description":"No time estimate"}]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
LinkControlAuditToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["control_id","audit_id"],"properties":{"audit_id":{"type":"string","format":"string"},"control_id":{"type":"string","format":"string"}}}`)
@@ -122,7 +122,7 @@ var (
ListRisksToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"filter":{"type":"object","properties":{"query":{"type":"string","description":"Search query"},"snapshot_id":{"type":"string","format":"string"}}},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["ASC","DESC"]},"field":{"type":"string","enum":["CREATED_AT","UPDATED_AT","NAME","CATEGORY","TREATMENT","INHERENT_RISK_SCORE","RESIDUAL_RISK_SCORE","OWNER_FULL_NAME"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`)
ListRisksToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["risks"],"properties":{"next_cursor":{"type":"string","format":"string"},"risks":{"type":"array","items":{"type":"object","required":["id","organization_id","name","category","treatment","inherent_likelihood","inherent_impact","inherent_risk_score","residual_likelihood","residual_impact","residual_risk_score","note","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Risk category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Risk description"},"id":{"type":"string","format":"string"},"inherent_impact":{"type":"integer","description":"Inherent impact"},"inherent_likelihood":{"type":"integer","description":"Inherent likelihood"},"inherent_risk_score":{"type":"integer","description":"Inherent risk score"},"name":{"type":"string","description":"Risk name"},"note":{"type":"string","description":"Risk note"},"organization_id":{"type":"string","format":"string"},"owner_id":{"anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No owner"}]},"residual_impact":{"type":"integer","description":"Residual impact"},"residual_likelihood":{"type":"integer","description":"Residual likelihood"},"residual_risk_score":{"type":"integer","description":"Residual risk score"},"snapshot_id":{"description":"Snapshot ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No snapshot"}]},"treatment":{"type":"string","enum":["MITIGATED","ACCEPTED","AVOIDED","TRANSFERRED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}}`)
ListSnapshotsToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["ASC","DESC"]},"field":{"type":"string","enum":["CREATED_AT","NAME","TYPE"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`)
ListSnapshotsToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["snapshots"],"properties":{"next_cursor":{"description":"Next page cursor","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"snapshots":{"type":"array","description":"List of snapshots","items":{"type":"object","required":["id","organization_id","name","type","created_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Snapshot description","anyOf":[{"type":"string","description":"Snapshot description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES"]}}}}}}`)
ListSnapshotsToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["snapshots"],"properties":{"next_cursor":{"description":"Next page cursor","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"snapshots":{"type":"array","description":"List of snapshots","items":{"type":"object","required":["id","organization_id","name","type","created_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Snapshot description","anyOf":[{"type":"string","description":"Snapshot description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES","STATES_OF_APPLICABILITY"]}}}}}}`)
ListTasksToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"measure_id":{"type":"string","format":"string"},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["ASC","DESC"]},"field":{"type":"string","enum":["CREATED_AT"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`)
ListTasksToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["tasks"],"properties":{"next_cursor":{"description":"Next page cursor","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"tasks":{"type":"array","description":"List of tasks","items":{"type":"object","required":["id","organization_id","name","state","created_at","updated_at"],"properties":{"assigned_to_id":{"description":"Assigned to person ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"Not assigned"}]},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"deadline":{"description":"Deadline","anyOf":[{"type":"string","description":"Deadline","format":"date-time"},{"type":"null","description":"No deadline"}]},"description":{"description":"Task description","anyOf":[{"type":"string","description":"Task description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"measure_id":{"description":"Measure ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No measure"}]},"name":{"type":"string","description":"Task name"},"organization_id":{"type":"string","format":"string"},"state":{"type":"string","enum":["TODO","DONE"]},"time_estimate":{"description":"Time estimate","anyOf":[{"type":"string","description":"A duration"},{"type":"null","description":"No time estimate"}]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}}`)
ListVendorsToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id"],"properties":{"cursor":{"type":"string","format":"string"},"filter":{"type":"object","properties":{"snapshot_id":{"type":"string","format":"string"}}},"order_by":{"type":"object","required":["field","direction"],"properties":{"direction":{"type":"string","enum":["ASC","DESC"]},"field":{"type":"string","enum":["CREATED_AT","UPDATED_AT","NAME"]}}},"organization_id":{"type":"string","format":"string"},"size":{"type":"integer","description":"Page size"}}}`)
@@ -131,8 +131,8 @@ var (
PublishDocumentVersionToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["document","document_version"],"properties":{"document":{"type":"object","required":["id","organization_id","owner_id","title","document_type","classification","trust_center_visibility","created_at","updated_at"],"properties":{"classification":{"type":"string","enum":["PUBLIC","INTERNAL","CONFIDENTIAL","SECRET"]},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"current_published_version":{"description":"Current published version number"},"document_type":{"type":"string","enum":["OTHER","ISMS","POLICY","PROCEDURE"]},"id":{"type":"string","format":"string"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"title":{"type":"string","description":"Document title"},"trust_center_visibility":{"type":"string","enum":["NONE","PRIVATE","PUBLIC"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}},"document_version":{"type":"object","required":["id","organization_id","document_id","title","owner_id","version_number","classification","content","changelog","status","created_at","updated_at"],"properties":{"changelog":{"type":"string","description":"Changelog"},"classification":{"type":"string","enum":["PUBLIC","INTERNAL","CONFIDENTIAL","SECRET"]},"content":{"type":"string","description":"Document content"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"document_id":{"type":"string","format":"string"},"id":{"type":"string","format":"string"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"published_at":{"description":"Published timestamp","format":"date-time"},"status":{"type":"string","enum":["DRAFT","PUBLISHED"]},"title":{"type":"string","description":"Document version title"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"},"version_number":{"type":"integer","description":"Version number"}}}}}`)
RequestDocumentVersionSignatureToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["document_version_id","signatory_id"],"properties":{"document_version_id":{"type":"string","format":"string"},"signatory_id":{"type":"string","format":"string"}}}`)
RequestDocumentVersionSignatureToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["document_version_signature"],"properties":{"document_version_signature":{"type":"object","required":["id","organization_id","document_version_id","state","signed_by","requested_at","created_at","updated_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"document_version_id":{"type":"string","format":"string"},"id":{"type":"string","format":"string"},"organization_id":{"type":"string","format":"string"},"requested_at":{"type":"string","description":"Requested timestamp","format":"date-time"},"signed_at":{"description":"Signed timestamp","format":"date-time"},"signed_by":{"type":"string","format":"string"},"state":{"type":"string","enum":["REQUESTED","SIGNED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
TakeSnapshotToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","name","type"],"properties":{"description":{"type":"string","description":"Snapshot description"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES"]}}}`)
TakeSnapshotToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["snapshot"],"properties":{"snapshot":{"type":"object","required":["id","organization_id","name","type","created_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Snapshot description","anyOf":[{"type":"string","description":"Snapshot description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES"]}}}}}`)
TakeSnapshotToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["organization_id","name","type"],"properties":{"description":{"type":"string","description":"Snapshot description"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES","STATES_OF_APPLICABILITY"]}}}`)
TakeSnapshotToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["snapshot"],"properties":{"snapshot":{"type":"object","required":["id","organization_id","name","type","created_at"],"properties":{"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Snapshot description","anyOf":[{"type":"string","description":"Snapshot description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Snapshot name"},"organization_id":{"type":"string","format":"string"},"type":{"type":"string","enum":["RISKS","VENDORS","ASSETS","DATA","NONCONFORMITIES","OBLIGATIONS","CONTINUAL_IMPROVEMENTS","PROCESSING_ACTIVITIES","STATES_OF_APPLICABILITY"]}}}}}`)
UnassignTaskToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"string"}}}`)
UnassignTaskToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["task"],"properties":{"task":{"type":"object","required":["id","organization_id","name","state","created_at","updated_at"],"properties":{"assigned_to_id":{"description":"Assigned to person ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"Not assigned"}]},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"deadline":{"description":"Deadline","anyOf":[{"type":"string","description":"Deadline","format":"date-time"},{"type":"null","description":"No deadline"}]},"description":{"description":"Task description","anyOf":[{"type":"string","description":"Task description"},{"type":"null","description":"No description"}]},"id":{"type":"string","format":"string"},"measure_id":{"description":"Measure ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No measure"}]},"name":{"type":"string","description":"Task name"},"organization_id":{"type":"string","format":"string"},"state":{"type":"string","enum":["TODO","DONE"]},"time_estimate":{"description":"Time estimate","anyOf":[{"type":"string","description":"A duration"},{"type":"null","description":"No time estimate"}]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
UnlinkControlAuditToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["control_id","audit_id"],"properties":{"audit_id":{"type":"string","format":"string"},"control_id":{"type":"string","format":"string"}}}`)
@@ -163,7 +163,7 @@ var (
UpdateMeasureToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["measure"],"properties":{"measure":{"type":"object","required":["id","category","name","state","created_at","updated_at"],"properties":{"category":{"type":"string","description":"Measure category"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"description":{"description":"Measure description"},"id":{"type":"string","format":"string"},"name":{"type":"string","description":"Measure name"},"state":{"type":"string","enum":["NOT_STARTED","IN_PROGRESS","NOT_APPLICABLE","IMPLEMENTED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
UpdateNonconformityToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"audit_id":{"description":"Audit ID","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"corrective_action":{"description":"Corrective action"},"date_identified":{"description":"Date identified","format":"date-time"},"description":{"description":"Description"},"due_date":{"description":"Due date","format":"date-time"},"effectiveness_check":{"description":"Effectiveness check"},"id":{"type":"string","format":"string"},"owner_id":{"description":"Owner ID","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"reference_id":{"type":"string","description":"Reference ID"},"root_cause":{"type":"string","description":"Root cause"},"status":{"type":"string","enum":["OPEN","IN_PROGRESS","CLOSED"]}}}`)
UpdateNonconformityToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["nonconformity"],"properties":{"nonconformity":{"type":"object","required":["id","organization_id","reference_id","root_cause","owner_id","status","created_at","updated_at"],"properties":{"audit_id":{"description":"Audit ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No audit"}]},"corrective_action":{"description":"Corrective action"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"date_identified":{"description":"Date identified","format":"date-time"},"description":{"description":"Description"},"due_date":{"description":"Due date","format":"date-time"},"effectiveness_check":{"description":"Effectiveness check"},"id":{"type":"string","format":"string"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"reference_id":{"type":"string","description":"Reference ID"},"root_cause":{"type":"string","description":"Root cause"},"snapshot_id":{"description":"Snapshot ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No snapshot"}]},"status":{"type":"string","enum":["OPEN","IN_PROGRESS","CLOSED"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
UpdateObligationToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"actions_to_be_implemented":{"description":"Actions to be implemented"},"area":{"description":"Area"},"due_date":{"description":"Due date","format":"date-time"},"id":{"type":"string","format":"string"},"last_review_date":{"description":"Last review date","format":"date-time"},"owner_id":{"description":"Owner ID","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"regulator":{"description":"Regulator"},"requirement":{"description":"Requirement"},"source":{"description":"Source"},"status":{"description":"Status","anyOf":[{"type":"string","enum":["NON_COMPLIANT","PARTIALLY_COMPLIANT","COMPLIANT"]},{"type":"null","description":"No status"}]}}}`)
UpdateObligationToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"actions_to_be_implemented":{"description":"Actions to be implemented"},"area":{"description":"Area"},"due_date":{"description":"Due date","format":"date-time"},"id":{"type":"string","format":"string"},"last_review_date":{"description":"Last review date","format":"date-time"},"owner_id":{"description":"Owner ID","anyOf":[{"type":"string","format":"string"},{"type":"null"}]},"regulator":{"description":"Regulator"},"requirement":{"description":"Requirement"},"source":{"description":"Source"},"status":{"description":"Status","anyOf":[{"type":"string","enum":["NON_COMPLIANT","PARTIALLY_COMPLIANT","COMPLIANT"]},{"type":"null","description":"No status"}]},"type":{"description":"Type","anyOf":[{"type":"string","enum":["LEGAL","CONTRACTUAL"]},{"type":"null","description":"No type"}]}}}`)
UpdateObligationToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["obligation"],"properties":{"obligation":{"type":"object","required":["id","organization_id","owner_id","status","created_at","updated_at"],"properties":{"actions_to_be_implemented":{"description":"Actions to be implemented"},"area":{"description":"Area"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"due_date":{"description":"Due date","format":"date-time"},"id":{"type":"string","format":"string"},"last_review_date":{"description":"Last review date","format":"date-time"},"organization_id":{"type":"string","format":"string"},"owner_id":{"type":"string","format":"string"},"regulator":{"description":"Regulator"},"requirement":{"description":"Requirement"},"snapshot_id":{"description":"Snapshot ID","anyOf":[{"type":"string","format":"string"},{"type":"null","description":"No snapshot"}]},"source":{"description":"Source"},"source_id":{"description":"Source ID"},"status":{"type":"string","enum":["NON_COMPLIANT","PARTIALLY_COMPLIANT","COMPLIANT"]},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
UpdatePeopleToolInputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["id"],"properties":{"additional_email_addresses":{"description":"Additional email addresses","anyOf":[{"type":"array","items":{"type":"string","format":"string"}},{"type":"null","description":"No additional email addresses"}]},"contract_end_date":{"description":"Contract end date","format":"date-time"},"contract_start_date":{"description":"Contract start date","format":"date-time"},"full_name":{"type":"string","description":"Full name"},"id":{"type":"string","format":"string"},"kind":{"type":"string","enum":["EMPLOYEE","CONTRACTOR","SERVICE_ACCOUNT"]},"position":{"description":"Position"},"primary_email_address":{"type":"string","format":"string"}}}`)
UpdatePeopleToolOutputSchema = mcp.MustUnmarshalSchema(`{"type":"object","required":["people"],"properties":{"people":{"type":"object","required":["id","organization_id","full_name","primary_email_address","additional_email_addresses","kind","created_at","updated_at"],"properties":{"additional_email_addresses":{"type":"array","description":"Additional email addresses","items":{"type":"string","format":"string"}},"contract_end_date":{"description":"Contract end date","format":"date-time"},"contract_start_date":{"description":"Contract start date","format":"date-time"},"created_at":{"type":"string","description":"Creation timestamp","format":"date-time"},"full_name":{"type":"string","description":"Full name"},"id":{"type":"string","format":"string"},"kind":{"type":"string","enum":["EMPLOYEE","CONTRACTOR","SERVICE_ACCOUNT"]},"organization_id":{"type":"string","format":"string"},"position":{"description":"Position"},"primary_email_address":{"type":"string","format":"string"},"updated_at":{"type":"string","description":"Update timestamp","format":"date-time"}}}}}`)
@@ -395,6 +395,8 @@ type AddObligationInput struct {
Source *string `json:"source,omitempty"`
// Status
Status *coredata.ObligationStatus `json:"status"`
// Type
Type *coredata.ObligationType `json:"type"`
}
// AddObligationOutput represents the schema
@@ -2016,6 +2018,8 @@ type UpdateObligationInput struct {
Source mcp.Omittable[*string] `json:"source,omitempty"`
// Status
Status *coredata.ObligationStatus `json:"status,omitempty"`
// Type
Type *coredata.ObligationType `json:"type,omitempty"`
}
// UpdateObligationOutput represents the schema