Add finding and obligation publish to document system
Replace the old snapshot-based approach with the new publish document system for findings and obligations. Includes GraphQL mutations, MCP tools, CLI commands, e2e tests, frontend publish dialogs, and snapshot-to-document migration tools. Remove snapshot mode entirely from findings and obligations: drop snapshotId from GraphQL schemas, filters, resolvers, MCP spec, frontend routes, pages, and helpers. The snapshot_id column remains in the database but is now filtered out with snapshot_id IS NULL. Remove auditor's ability to publish SoA. Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/finding/create"
|
||||
"go.probo.inc/probo/pkg/cmd/finding/delete"
|
||||
"go.probo.inc/probo/pkg/cmd/finding/list"
|
||||
"go.probo.inc/probo/pkg/cmd/finding/publish"
|
||||
"go.probo.inc/probo/pkg/cmd/finding/update"
|
||||
"go.probo.inc/probo/pkg/cmd/finding/view"
|
||||
)
|
||||
@@ -35,6 +36,7 @@ func NewCmdFinding(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(view.NewCmdView(f))
|
||||
cmd.AddCommand(update.NewCmdUpdate(f))
|
||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||
cmd.AddCommand(publish.NewCmdPublish(f))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
147
pkg/cmd/finding/publish/publish.go
Normal file
147
pkg/cmd/finding/publish/publish.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) 2026 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 publish
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const publishMutation = `
|
||||
mutation($input: PublishFindingListInput!) {
|
||||
publishFindingList(input: $input) {
|
||||
documentEdge {
|
||||
node {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
documentVersionEdge {
|
||||
node {
|
||||
id
|
||||
title
|
||||
major
|
||||
minor
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type publishResponse struct {
|
||||
PublishFindingList struct {
|
||||
DocumentEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
} `json:"node"`
|
||||
} `json:"documentEdge"`
|
||||
DocumentVersionEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Status string `json:"status"`
|
||||
} `json:"node"`
|
||||
} `json:"documentVersionEdge"`
|
||||
} `json:"publishFindingList"`
|
||||
}
|
||||
|
||||
func NewCmdPublish(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagOrg string
|
||||
flagApprover []string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "publish",
|
||||
Short: "Publish the finding register as a document version",
|
||||
Example: ` # Publish the finding register
|
||||
prb finding publish --org ORG_ID
|
||||
|
||||
# Publish with approvers
|
||||
prb finding publish --org ORG_ID --approver PROFILE_ID1 --approver PROFILE_ID2`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if flagOrg == "" {
|
||||
flagOrg = hc.Organization
|
||||
}
|
||||
if flagOrg == "" {
|
||||
return fmt.Errorf("organization is required: pass --org or run `prb auth login`")
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
)
|
||||
|
||||
input := map[string]any{
|
||||
"organizationId": flagOrg,
|
||||
}
|
||||
|
||||
if len(flagApprover) > 0 {
|
||||
input["approverIds"] = flagApprover
|
||||
}
|
||||
|
||||
data, err := client.Do(
|
||||
publishMutation,
|
||||
map[string]any{"input": input},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp publishResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
v := resp.PublishFindingList.DocumentVersionEdge.Node
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Published finding register %s (v%d.%d)\n",
|
||||
v.Title,
|
||||
v.Major,
|
||||
v.Minor,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
||||
cmd.Flags().StringArrayVar(&flagApprover, "approver", nil, "Approver profile ID (can be repeated)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
)
|
||||
|
||||
const listQuery = `
|
||||
query($id: ID!, $first: Int, $after: CursorKey, $orderBy: ObligationOrder, $filter: ObligationFilter) {
|
||||
query($id: ID!, $first: Int, $after: CursorKey, $orderBy: ObligationOrder) {
|
||||
node(id: $id) {
|
||||
__typename
|
||||
... on Organization {
|
||||
obligations(first: $first, after: $after, orderBy: $orderBy, filter: $filter) {
|
||||
obligations(first: $first, after: $after, orderBy: $orderBy) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/cmd/obligation/create"
|
||||
"go.probo.inc/probo/pkg/cmd/obligation/delete"
|
||||
"go.probo.inc/probo/pkg/cmd/obligation/list"
|
||||
"go.probo.inc/probo/pkg/cmd/obligation/publish"
|
||||
"go.probo.inc/probo/pkg/cmd/obligation/update"
|
||||
"go.probo.inc/probo/pkg/cmd/obligation/view"
|
||||
)
|
||||
@@ -35,6 +36,7 @@ func NewCmdObligation(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.AddCommand(view.NewCmdView(f))
|
||||
cmd.AddCommand(update.NewCmdUpdate(f))
|
||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||
cmd.AddCommand(publish.NewCmdPublish(f))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
147
pkg/cmd/obligation/publish/publish.go
Normal file
147
pkg/cmd/obligation/publish/publish.go
Normal file
@@ -0,0 +1,147 @@
|
||||
// Copyright (c) 2026 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 publish
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.probo.inc/probo/pkg/cli/api"
|
||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||
)
|
||||
|
||||
const publishMutation = `
|
||||
mutation($input: PublishObligationListInput!) {
|
||||
publishObligationList(input: $input) {
|
||||
documentEdge {
|
||||
node {
|
||||
id
|
||||
status
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
documentVersionEdge {
|
||||
node {
|
||||
id
|
||||
title
|
||||
major
|
||||
minor
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type publishResponse struct {
|
||||
PublishObligationList struct {
|
||||
DocumentEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
} `json:"node"`
|
||||
} `json:"documentEdge"`
|
||||
DocumentVersionEdge struct {
|
||||
Node struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
Status string `json:"status"`
|
||||
} `json:"node"`
|
||||
} `json:"documentVersionEdge"`
|
||||
} `json:"publishObligationList"`
|
||||
}
|
||||
|
||||
func NewCmdPublish(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagOrg string
|
||||
flagApprover []string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "publish",
|
||||
Short: "Publish the obligation register as a document version",
|
||||
Example: ` # Publish the obligation register
|
||||
prb obligation publish --org ORG_ID
|
||||
|
||||
# Publish with approvers
|
||||
prb obligation publish --org ORG_ID --approver PROFILE_ID1 --approver PROFILE_ID2`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, hc, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if flagOrg == "" {
|
||||
flagOrg = hc.Organization
|
||||
}
|
||||
if flagOrg == "" {
|
||||
return fmt.Errorf("organization is required: pass --org or run `prb auth login`")
|
||||
}
|
||||
|
||||
client := api.NewClient(
|
||||
host,
|
||||
hc.Token,
|
||||
"/api/console/v1/graphql",
|
||||
cfg.HTTPTimeoutDuration(),
|
||||
)
|
||||
|
||||
input := map[string]any{
|
||||
"organizationId": flagOrg,
|
||||
}
|
||||
|
||||
if len(flagApprover) > 0 {
|
||||
input["approverIds"] = flagApprover
|
||||
}
|
||||
|
||||
data, err := client.Do(
|
||||
publishMutation,
|
||||
map[string]any{"input": input},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp publishResponse
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return fmt.Errorf("cannot parse response: %w", err)
|
||||
}
|
||||
|
||||
v := resp.PublishObligationList.DocumentVersionEdge.Node
|
||||
_, _ = fmt.Fprintf(
|
||||
f.IOStreams.Out,
|
||||
"Published obligation register %s (v%d.%d)\n",
|
||||
v.Title,
|
||||
v.Major,
|
||||
v.Minor,
|
||||
)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
|
||||
cmd.Flags().StringArrayVar(&flagApprover, "approver", nil, "Approver profile ID (can be repeated)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -120,6 +120,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND id = @finding_id
|
||||
AND snapshot_id IS NULL
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
@@ -158,6 +159,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
AND %s
|
||||
`
|
||||
|
||||
@@ -212,6 +214,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
@@ -412,95 +415,6 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs Findings) Snapshot(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error {
|
||||
query := `
|
||||
INSERT INTO findings (
|
||||
id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
organization_id,
|
||||
kind,
|
||||
reference_id,
|
||||
description,
|
||||
source,
|
||||
identified_on,
|
||||
root_cause,
|
||||
corrective_action,
|
||||
owner_id,
|
||||
due_date,
|
||||
status,
|
||||
priority,
|
||||
risk_id,
|
||||
effectiveness_check,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(@tenant_id), @finding_entity_type),
|
||||
@tenant_id,
|
||||
@snapshot_id,
|
||||
f.id,
|
||||
f.organization_id,
|
||||
f.kind,
|
||||
f.reference_id,
|
||||
f.description,
|
||||
f.source,
|
||||
f.identified_on,
|
||||
f.root_cause,
|
||||
f.corrective_action,
|
||||
f.owner_id,
|
||||
f.due_date,
|
||||
f.status,
|
||||
f.priority,
|
||||
f.risk_id,
|
||||
f.effectiveness_check,
|
||||
f.created_at,
|
||||
f.updated_at
|
||||
FROM findings f
|
||||
WHERE %s AND f.organization_id = @organization_id AND f.snapshot_id IS NULL
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
"finding_entity_type": FindingEntityType,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, query, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert finding snapshots: %w", err)
|
||||
}
|
||||
|
||||
auditQuery := `
|
||||
INSERT INTO findings_audits (finding_id, audit_id, reference_id, organization_id, tenant_id, created_at)
|
||||
SELECT
|
||||
snap.id,
|
||||
fa.audit_id,
|
||||
fa.reference_id,
|
||||
fa.organization_id,
|
||||
fa.tenant_id,
|
||||
fa.created_at
|
||||
FROM findings_audits fa
|
||||
JOIN findings live ON fa.finding_id = live.id AND live.snapshot_id IS NULL
|
||||
JOIN findings snap ON snap.source_id = live.id AND snap.snapshot_id = @snapshot_id
|
||||
WHERE %s AND live.organization_id = @organization_id
|
||||
`
|
||||
|
||||
auditQuery = fmt.Sprintf(auditQuery, scope.SQLFragment())
|
||||
|
||||
_, err = conn.Exec(ctx, auditQuery, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert finding audit snapshots: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *Findings) LoadByAuditID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
@@ -538,6 +452,7 @@ WITH f AS (
|
||||
findings_audits fa ON fi.id = fa.finding_id
|
||||
WHERE
|
||||
fa.audit_id = @audit_id
|
||||
AND fi.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
@@ -605,6 +520,7 @@ WITH f AS (
|
||||
findings_audits fa ON fi.id = fa.finding_id
|
||||
WHERE
|
||||
fa.audit_id = @audit_id
|
||||
AND fi.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
@@ -631,3 +547,167 @@ WHERE
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (fs *Findings) LoadAllByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
kind,
|
||||
reference_id,
|
||||
description,
|
||||
source,
|
||||
identified_on,
|
||||
root_cause,
|
||||
corrective_action,
|
||||
owner_id,
|
||||
due_date,
|
||||
status,
|
||||
priority,
|
||||
risk_id,
|
||||
effectiveness_check,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
findings
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
ORDER BY
|
||||
reference_id ASC
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query findings: %w", err)
|
||||
}
|
||||
|
||||
findings, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Finding])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect findings: %w", err)
|
||||
}
|
||||
|
||||
*fs = findings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Finding) GetGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
organizationID gid.GID,
|
||||
) (*gid.GID, error) {
|
||||
var documentID *gid.GID
|
||||
|
||||
err := conn.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
findings_document_id
|
||||
FROM
|
||||
generated_documents
|
||||
WHERE
|
||||
organization_id = @organization_id
|
||||
`,
|
||||
pgx.NamedArgs{"organization_id": organizationID},
|
||||
).Scan(&documentID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get finding list document ID: %w", err)
|
||||
}
|
||||
|
||||
return documentID, nil
|
||||
}
|
||||
|
||||
func (f Finding) UpsertGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
organizationID gid.GID,
|
||||
tenantID gid.TenantID,
|
||||
documentID gid.GID,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO generated_documents (
|
||||
organization_id,
|
||||
tenant_id,
|
||||
findings_document_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@findings_document_id,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
ON CONFLICT (organization_id) DO UPDATE
|
||||
SET
|
||||
findings_document_id = @findings_document_id,
|
||||
updated_at = @updated_at
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"organization_id": organizationID,
|
||||
"tenant_id": tenantID,
|
||||
"findings_document_id": documentID,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot upsert finding list document ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f Finding) ClearGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
documentIDs []gid.GID,
|
||||
) error {
|
||||
ids := make([]string, len(documentIDs))
|
||||
for i, id := range documentIDs {
|
||||
ids[i] = id.String()
|
||||
}
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
UPDATE
|
||||
generated_documents
|
||||
SET
|
||||
findings_document_id = NULL,
|
||||
updated_at = @now
|
||||
WHERE
|
||||
findings_document_id = ANY(@ids)
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"ids": ids,
|
||||
"now": time.Now(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot clear finding list document references: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,34 +21,29 @@ import (
|
||||
|
||||
type (
|
||||
FindingFilter struct {
|
||||
snapshotID **gid.GID
|
||||
kind *FindingKind
|
||||
status *FindingStatus
|
||||
priority *FindingPriority
|
||||
ownerID *gid.GID
|
||||
kind *FindingKind
|
||||
status *FindingStatus
|
||||
priority *FindingPriority
|
||||
ownerID *gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewFindingFilter(
|
||||
snapshotID **gid.GID,
|
||||
kind *FindingKind,
|
||||
status *FindingStatus,
|
||||
priority *FindingPriority,
|
||||
ownerID *gid.GID,
|
||||
) *FindingFilter {
|
||||
return &FindingFilter{
|
||||
snapshotID: snapshotID,
|
||||
kind: kind,
|
||||
status: status,
|
||||
priority: priority,
|
||||
ownerID: ownerID,
|
||||
kind: kind,
|
||||
status: status,
|
||||
priority: priority,
|
||||
ownerID: ownerID,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FindingFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args := pgx.StrictNamedArgs{
|
||||
"has_snapshot_filter": false,
|
||||
"filter_snapshot_id": nil,
|
||||
"has_kind_filter": false,
|
||||
"filter_kind": nil,
|
||||
"has_status_filter": false,
|
||||
@@ -59,13 +54,6 @@ func (f *FindingFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
"filter_owner_id": nil,
|
||||
}
|
||||
|
||||
if f.snapshotID != nil {
|
||||
args["has_snapshot_filter"] = true
|
||||
if *f.snapshotID != nil {
|
||||
args["filter_snapshot_id"] = **f.snapshotID
|
||||
}
|
||||
}
|
||||
|
||||
if f.kind != nil {
|
||||
args["has_kind_filter"] = true
|
||||
args["filter_kind"] = string(*f.kind)
|
||||
@@ -92,15 +80,6 @@ func (f *FindingFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
func (f *FindingFilter) 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
|
||||
AND
|
||||
CASE
|
||||
WHEN @has_kind_filter::boolean = false THEN TRUE
|
||||
WHEN @has_kind_filter::boolean = true THEN
|
||||
|
||||
17
pkg/coredata/migrations/20260422T130000Z.sql
Normal file
17
pkg/coredata/migrations/20260422T130000Z.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Copyright (c) 2026 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.
|
||||
|
||||
ALTER TABLE generated_documents
|
||||
ADD COLUMN findings_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
|
||||
ADD COLUMN obligations_document_id TEXT REFERENCES documents(id) ON DELETE SET NULL;
|
||||
@@ -108,6 +108,7 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND id = @obligation_id
|
||||
AND snapshot_id IS NULL
|
||||
LIMIT 1;
|
||||
`
|
||||
|
||||
@@ -136,7 +137,6 @@ func (os *Obligations) CountByOrganizationID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -146,14 +146,13 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND snapshot_id IS NULL
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
@@ -171,7 +170,6 @@ func (os *Obligations) CountByRiskID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
riskID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -186,20 +184,19 @@ WITH obls AS (
|
||||
risks_obligations ro ON o.id = ro.obligation_id
|
||||
WHERE
|
||||
ro.risk_id = @risk_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"risk_id": riskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
@@ -218,7 +215,6 @@ func (os *Obligations) LoadByOrganizationID(
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -243,15 +239,14 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND %s
|
||||
AND snapshot_id IS NULL
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"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)
|
||||
@@ -275,7 +270,6 @@ func (os *Obligations) LoadByRiskID(
|
||||
scope Scoper,
|
||||
riskID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -304,6 +298,7 @@ WITH obls AS (
|
||||
risks_obligations ro ON o.id = ro.obligation_id
|
||||
WHERE
|
||||
ro.risk_id = @risk_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
@@ -326,14 +321,12 @@ FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"risk_id": riskID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
maps.Copy(args, cursor.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
@@ -356,7 +349,6 @@ func (os *Obligations) CountByControlID(
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -370,20 +362,19 @@ WITH obls AS (
|
||||
controls_obligations co ON o.id = co.obligation_id
|
||||
WHERE
|
||||
co.control_id = @control_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
COUNT(id)
|
||||
FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"control_id": controlID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
row := conn.QueryRow(ctx, q, args)
|
||||
|
||||
@@ -402,7 +393,6 @@ func (os *Obligations) LoadByControlID(
|
||||
scope Scoper,
|
||||
controlID gid.GID,
|
||||
cursor *page.Cursor[ObligationOrderField],
|
||||
filter *ObligationFilter,
|
||||
) error {
|
||||
q := `
|
||||
WITH obls AS (
|
||||
@@ -430,6 +420,7 @@ WITH obls AS (
|
||||
controls_obligations co ON o.id = co.obligation_id
|
||||
WHERE
|
||||
co.control_id = @control_id
|
||||
AND o.snapshot_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
@@ -452,13 +443,11 @@ FROM
|
||||
obls
|
||||
WHERE %s
|
||||
AND %s
|
||||
AND %s
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment(), cursor.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.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)
|
||||
@@ -625,13 +614,15 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (os Obligations) Snapshot(ctx context.Context, conn pg.Tx, scope Scoper, organizationID, snapshotID gid.GID) error {
|
||||
query := `
|
||||
INSERT INTO obligations (
|
||||
func (os *Obligations) LoadAllByOrganizationID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
organization_id,
|
||||
area,
|
||||
source,
|
||||
@@ -643,44 +634,142 @@ INSERT INTO obligations (
|
||||
due_date,
|
||||
status,
|
||||
type,
|
||||
snapshot_id,
|
||||
source_id,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
generate_gid(decode_base64_unpadded(@tenant_id), @obligation_entity_type),
|
||||
@tenant_id,
|
||||
@snapshot_id,
|
||||
o.id,
|
||||
o.organization_id,
|
||||
o.area,
|
||||
o.source,
|
||||
o.requirement,
|
||||
o.actions_to_be_implemented,
|
||||
o.regulator,
|
||||
o.owner_profile_id,
|
||||
o.last_review_date,
|
||||
o.due_date,
|
||||
o.status,
|
||||
o.type,
|
||||
o.created_at,
|
||||
o.updated_at
|
||||
FROM obligations o
|
||||
WHERE %s AND o.organization_id = @organization_id AND o.snapshot_id IS NULL
|
||||
`
|
||||
FROM
|
||||
obligations
|
||||
WHERE
|
||||
%s
|
||||
AND organization_id = @organization_id
|
||||
AND snapshot_id IS NULL
|
||||
ORDER BY
|
||||
created_at ASC
|
||||
`
|
||||
|
||||
query = fmt.Sprintf(query, scope.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"snapshot_id": snapshotID,
|
||||
"organization_id": organizationID,
|
||||
"obligation_entity_type": ObligationEntityType,
|
||||
}
|
||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, query, args)
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert obligation snapshots: %w", err)
|
||||
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) GetGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
organizationID gid.GID,
|
||||
) (*gid.GID, error) {
|
||||
var documentID *gid.GID
|
||||
|
||||
err := conn.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
obligations_document_id
|
||||
FROM
|
||||
generated_documents
|
||||
WHERE
|
||||
organization_id = @organization_id
|
||||
`,
|
||||
pgx.NamedArgs{"organization_id": organizationID},
|
||||
).Scan(&documentID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get obligation list document ID: %w", err)
|
||||
}
|
||||
|
||||
return documentID, nil
|
||||
}
|
||||
|
||||
func (o Obligation) UpsertGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
organizationID gid.GID,
|
||||
tenantID gid.TenantID,
|
||||
documentID gid.GID,
|
||||
) error {
|
||||
now := time.Now()
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO generated_documents (
|
||||
organization_id,
|
||||
tenant_id,
|
||||
obligations_document_id,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@organization_id,
|
||||
@tenant_id,
|
||||
@obligations_document_id,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
ON CONFLICT (organization_id) DO UPDATE
|
||||
SET
|
||||
obligations_document_id = @obligations_document_id,
|
||||
updated_at = @updated_at
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"organization_id": organizationID,
|
||||
"tenant_id": tenantID,
|
||||
"obligations_document_id": documentID,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot upsert obligation list document ID: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o Obligation) ClearGeneratedDocumentID(
|
||||
ctx context.Context,
|
||||
conn pg.Tx,
|
||||
documentIDs []gid.GID,
|
||||
) error {
|
||||
ids := make([]string, len(documentIDs))
|
||||
for i, id := range documentIDs {
|
||||
ids[i] = id.String()
|
||||
}
|
||||
|
||||
_, err := conn.Exec(
|
||||
ctx,
|
||||
`
|
||||
UPDATE
|
||||
generated_documents
|
||||
SET
|
||||
obligations_document_id = NULL,
|
||||
updated_at = @now
|
||||
WHERE
|
||||
obligations_document_id = ANY(@ids)
|
||||
`,
|
||||
pgx.NamedArgs{
|
||||
"ids": ids,
|
||||
"now": time.Now(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot clear obligation list document references: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) 2025-2026 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 (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
ObligationFilter struct {
|
||||
snapshotID **gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewObligationFilter(snapshotID **gid.GID) *ObligationFilter {
|
||||
return &ObligationFilter{
|
||||
snapshotID: snapshotID,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *ObligationFilter) SQLArguments() pgx.NamedArgs {
|
||||
args := pgx.NamedArgs{}
|
||||
|
||||
if f.snapshotID != nil && *f.snapshotID != nil {
|
||||
args["filter_snapshot_id"] = **f.snapshotID
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func (f *ObligationFilter) SQLFragment() string {
|
||||
if f.snapshotID == nil {
|
||||
return "TRUE"
|
||||
}
|
||||
|
||||
if *f.snapshotID == nil {
|
||||
return "snapshot_id IS NULL"
|
||||
} else {
|
||||
return "snapshot_id = @filter_snapshot_id"
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,6 @@ func SnapshotsTypes() []SnapshotsType {
|
||||
return []SnapshotsType{
|
||||
SnapshotsTypeRisks,
|
||||
SnapshotsTypeVendors,
|
||||
SnapshotsTypeFindings,
|
||||
SnapshotsTypeObligations,
|
||||
SnapshotsTypeProcessingActivities,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ func GetSnapshottable(snapshotType SnapshotsType) (Snapshottable, error) {
|
||||
switch snapshotType {
|
||||
case SnapshotsTypeRisks:
|
||||
return Risks{}, nil
|
||||
case SnapshotsTypeFindings:
|
||||
return Findings{}, nil
|
||||
case SnapshotsTypeObligations:
|
||||
return Obligations{}, nil
|
||||
case SnapshotsTypeProcessingActivities:
|
||||
return ProcessingActivities{}, nil
|
||||
case SnapshotsTypeVendors:
|
||||
|
||||
@@ -338,6 +338,49 @@ type (
|
||||
Owner string
|
||||
Vendors string
|
||||
}
|
||||
|
||||
FindingListData struct {
|
||||
Title string
|
||||
OrganizationName string
|
||||
CreatedAt time.Time
|
||||
TotalFindings int
|
||||
Rows []FindingListRow
|
||||
}
|
||||
|
||||
FindingListRow struct {
|
||||
ReferenceID string
|
||||
Kind string
|
||||
Description string
|
||||
Source string
|
||||
IdentifiedOn string
|
||||
RootCause string
|
||||
CorrectiveAction string
|
||||
EffectivenessCheck string
|
||||
Status string
|
||||
Priority string
|
||||
Owner string
|
||||
DueDate string
|
||||
}
|
||||
|
||||
ObligationListData struct {
|
||||
Title string
|
||||
OrganizationName string
|
||||
CreatedAt time.Time
|
||||
TotalObligations int
|
||||
Rows []ObligationListRow
|
||||
}
|
||||
|
||||
ObligationListRow struct {
|
||||
Area string
|
||||
Source string
|
||||
Requirement string
|
||||
ActionsToBeImplemented string
|
||||
Status string
|
||||
Type string
|
||||
Regulator string
|
||||
Owner string
|
||||
DueDate string
|
||||
}
|
||||
)
|
||||
|
||||
func BoolLabel(v bool) string {
|
||||
|
||||
@@ -264,13 +264,15 @@ const (
|
||||
ActionFindingDelete = "core:finding:delete"
|
||||
ActionFindingAuditMappingCreate = "core:finding:create-audit-mapping"
|
||||
ActionFindingAuditMappingDelete = "core:finding:delete-audit-mapping"
|
||||
ActionFindingPublish = "core:finding:publish"
|
||||
|
||||
// Obligation actions
|
||||
ActionObligationGet = "core:obligation:get"
|
||||
ActionObligationList = "core:obligation:list"
|
||||
ActionObligationCreate = "core:obligation:create"
|
||||
ActionObligationUpdate = "core:obligation:update"
|
||||
ActionObligationDelete = "core:obligation:delete"
|
||||
ActionObligationGet = "core:obligation:get"
|
||||
ActionObligationList = "core:obligation:list"
|
||||
ActionObligationCreate = "core:obligation:create"
|
||||
ActionObligationUpdate = "core:obligation:update"
|
||||
ActionObligationDelete = "core:obligation:delete"
|
||||
ActionObligationPublish = "core:obligation:publish"
|
||||
|
||||
// ProcessingActivity actions
|
||||
ActionProcessingActivityList = "core:processing-activity:list"
|
||||
|
||||
@@ -1295,6 +1295,16 @@ func (s *DocumentService) clearDocumentReferences(
|
||||
return err
|
||||
}
|
||||
|
||||
finding := coredata.Finding{}
|
||||
if err := finding.ClearGeneratedDocumentID(ctx, tx, documentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obligation := coredata.Obligation{}
|
||||
if err := obligation.ClearGeneratedDocumentID(ctx, tx, documentIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
soa := coredata.StatementOfApplicability{}
|
||||
if err := soa.ClearDocumentIDByDocumentIDs(ctx, tx, documentIDs); err != nil {
|
||||
return err
|
||||
|
||||
@@ -936,3 +936,683 @@ func BuildStatementOfApplicabilityDocument(data docgen.StatementOfApplicabilityD
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (s *GeneratedDocumentService) PublishFindingList(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
approverIDs []gid.GID,
|
||||
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||
var (
|
||||
document *coredata.Document
|
||||
documentVersion *coredata.DocumentVersion
|
||||
)
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil {
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
}
|
||||
|
||||
documentData, err := s.buildFindingListDocumentData(ctx, tx, organization)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build document data: %w", err)
|
||||
}
|
||||
|
||||
prosemirrorJSON, err := BuildFindingListDocument(documentData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build prosemirror document: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
finding := coredata.Finding{}
|
||||
findingDocumentID, err := finding.GetGeneratedDocumentID(ctx, tx, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query generated documents: %w", err)
|
||||
}
|
||||
|
||||
var existingDoc *coredata.Document
|
||||
if findingDocumentID != nil {
|
||||
doc := &coredata.Document{}
|
||||
err = doc.LoadByID(ctx, tx, s.svc.scope, *findingDocumentID)
|
||||
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load finding list document: %w", err)
|
||||
}
|
||||
|
||||
if err == nil && doc.ArchivedAt == nil {
|
||||
existingDoc = doc
|
||||
} else {
|
||||
if err := finding.ClearGeneratedDocumentID(ctx, tx, []gid.GID{*findingDocumentID}); err != nil {
|
||||
return fmt.Errorf("cannot clear document reference: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasApprovers := len(approverIDs) > 0
|
||||
|
||||
if existingDoc == nil {
|
||||
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
|
||||
|
||||
document = &coredata.Document{
|
||||
ID: documentID,
|
||||
OrganizationID: organizationID,
|
||||
WriteMode: coredata.DocumentWriteModeGenerated,
|
||||
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
|
||||
Status: coredata.DocumentStatusActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := document.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||
return fmt.Errorf("cannot insert document: %w", err)
|
||||
}
|
||||
|
||||
if err := finding.UpsertGeneratedDocumentID(ctx, tx, organizationID, s.svc.scope.GetTenantID(), documentID); err != nil {
|
||||
return fmt.Errorf("cannot upsert generated documents: %w", err)
|
||||
}
|
||||
} else {
|
||||
document = existingDoc
|
||||
}
|
||||
|
||||
var newMajor int
|
||||
if document.CurrentPublishedMajor != nil {
|
||||
newMajor = *document.CurrentPublishedMajor + 1
|
||||
} else {
|
||||
newMajor = 1
|
||||
}
|
||||
|
||||
versionStatus := coredata.DocumentVersionStatusPublished
|
||||
var publishedAt *time.Time
|
||||
if hasApprovers {
|
||||
versionStatus = coredata.DocumentVersionStatusDraft
|
||||
} else {
|
||||
publishedAt = &now
|
||||
}
|
||||
|
||||
documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
|
||||
documentVersion = &coredata.DocumentVersion{
|
||||
ID: documentVersionID,
|
||||
OrganizationID: organizationID,
|
||||
DocumentID: document.ID,
|
||||
Title: "Finding List",
|
||||
Major: newMajor,
|
||||
Minor: 0,
|
||||
Content: prosemirrorJSON,
|
||||
Status: versionStatus,
|
||||
Classification: coredata.DocumentClassificationConfidential,
|
||||
DocumentType: coredata.DocumentTypeRegister,
|
||||
Orientation: coredata.DocumentVersionOrientationLandscape,
|
||||
PublishedAt: publishedAt,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
||||
}
|
||||
return fmt.Errorf("cannot insert document version: %w", err)
|
||||
}
|
||||
|
||||
if hasApprovers {
|
||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
||||
}
|
||||
|
||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
||||
ctx,
|
||||
tx,
|
||||
document,
|
||||
documentVersion,
|
||||
approverIDs,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot request approval: %w", err)
|
||||
}
|
||||
} else {
|
||||
document.CurrentPublishedMajor = &newMajor
|
||||
document.CurrentPublishedMinor = new(0)
|
||||
document.UpdatedAt = now
|
||||
|
||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
||||
return fmt.Errorf("cannot update document: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return document, documentVersion, nil
|
||||
}
|
||||
|
||||
func (s *GeneratedDocumentService) GetFindingsDocumentID(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
) (*gid.GID, error) {
|
||||
var findingDocumentID *gid.GID
|
||||
|
||||
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||
finding := coredata.Finding{}
|
||||
var err error
|
||||
findingDocumentID, err = finding.GetGeneratedDocumentID(ctx, conn, organizationID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get finding list document ID: %w", err)
|
||||
}
|
||||
|
||||
return findingDocumentID, nil
|
||||
}
|
||||
|
||||
func (s *GeneratedDocumentService) buildFindingListDocumentData(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
organization *coredata.Organization,
|
||||
) (docgen.FindingListData, error) {
|
||||
var findings coredata.Findings
|
||||
if err := findings.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organization.ID); err != nil {
|
||||
return docgen.FindingListData{}, fmt.Errorf("cannot load findings: %w", err)
|
||||
}
|
||||
|
||||
if len(findings) == 0 {
|
||||
return docgen.FindingListData{
|
||||
Title: "Finding List",
|
||||
OrganizationName: organization.Name,
|
||||
CreatedAt: time.Now(),
|
||||
TotalFindings: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ownerIDs := make([]gid.GID, 0, len(findings))
|
||||
ownerIDSet := make(map[gid.GID]struct{})
|
||||
for _, f := range findings {
|
||||
if f.OwnerID != nil {
|
||||
if _, ok := ownerIDSet[*f.OwnerID]; !ok {
|
||||
ownerIDs = append(ownerIDs, *f.OwnerID)
|
||||
ownerIDSet[*f.OwnerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profileMap := make(map[gid.GID]*coredata.MembershipProfile)
|
||||
if len(ownerIDs) > 0 {
|
||||
var profiles coredata.MembershipProfiles
|
||||
if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil {
|
||||
return docgen.FindingListData{}, fmt.Errorf("cannot load profiles: %w", err)
|
||||
}
|
||||
|
||||
for _, p := range profiles {
|
||||
profileMap[p.ID] = p
|
||||
}
|
||||
}
|
||||
|
||||
rows := make([]docgen.FindingListRow, 0, len(findings))
|
||||
for _, f := range findings {
|
||||
ownerName := "-"
|
||||
if f.OwnerID != nil {
|
||||
if p, ok := profileMap[*f.OwnerID]; ok {
|
||||
ownerName = p.FullName
|
||||
}
|
||||
}
|
||||
|
||||
description := "-"
|
||||
if f.Description != nil && *f.Description != "" {
|
||||
description = *f.Description
|
||||
}
|
||||
|
||||
source := "-"
|
||||
if f.Source != nil && *f.Source != "" {
|
||||
source = *f.Source
|
||||
}
|
||||
|
||||
identifiedOn := "-"
|
||||
if f.IdentifiedOn != nil {
|
||||
identifiedOn = f.IdentifiedOn.Format("2006-01-02")
|
||||
}
|
||||
|
||||
rootCause := "-"
|
||||
if f.RootCause != nil && *f.RootCause != "" {
|
||||
rootCause = *f.RootCause
|
||||
}
|
||||
|
||||
correctiveAction := "-"
|
||||
if f.CorrectiveAction != nil && *f.CorrectiveAction != "" {
|
||||
correctiveAction = *f.CorrectiveAction
|
||||
}
|
||||
|
||||
effectivenessCheck := "-"
|
||||
if f.EffectivenessCheck != nil && *f.EffectivenessCheck != "" {
|
||||
effectivenessCheck = *f.EffectivenessCheck
|
||||
}
|
||||
|
||||
dueDate := "-"
|
||||
if f.DueDate != nil {
|
||||
dueDate = f.DueDate.Format("2006-01-02")
|
||||
}
|
||||
|
||||
rows = append(rows, docgen.FindingListRow{
|
||||
ReferenceID: f.ReferenceID,
|
||||
Kind: formatFindingKind(f.Kind),
|
||||
Description: description,
|
||||
Source: source,
|
||||
IdentifiedOn: identifiedOn,
|
||||
RootCause: rootCause,
|
||||
CorrectiveAction: correctiveAction,
|
||||
EffectivenessCheck: effectivenessCheck,
|
||||
Status: formatFindingStatus(f.Status),
|
||||
Priority: formatFindingPriority(f.Priority),
|
||||
Owner: ownerName,
|
||||
DueDate: dueDate,
|
||||
})
|
||||
}
|
||||
|
||||
return docgen.FindingListData{
|
||||
Title: "Finding List",
|
||||
OrganizationName: organization.Name,
|
||||
CreatedAt: time.Now(),
|
||||
TotalFindings: len(findings),
|
||||
Rows: rows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func formatFindingKind(k coredata.FindingKind) string {
|
||||
switch k {
|
||||
case coredata.FindingKindMinorNonconformity:
|
||||
return "Minor Nonconformity"
|
||||
case coredata.FindingKindMajorNonconformity:
|
||||
return "Major Nonconformity"
|
||||
case coredata.FindingKindObservation:
|
||||
return "Observation"
|
||||
case coredata.FindingKindException:
|
||||
return "Exception"
|
||||
default:
|
||||
return string(k)
|
||||
}
|
||||
}
|
||||
|
||||
func formatFindingStatus(s coredata.FindingStatus) string {
|
||||
switch s {
|
||||
case coredata.FindingStatusOpen:
|
||||
return "Open"
|
||||
case coredata.FindingStatusInProgress:
|
||||
return "In Progress"
|
||||
case coredata.FindingStatusClosed:
|
||||
return "Closed"
|
||||
case coredata.FindingStatusRiskAccepted:
|
||||
return "Risk Accepted"
|
||||
case coredata.FindingStatusMitigated:
|
||||
return "Mitigated"
|
||||
case coredata.FindingStatusFalsePositive:
|
||||
return "False Positive"
|
||||
default:
|
||||
return string(s)
|
||||
}
|
||||
}
|
||||
|
||||
func formatFindingPriority(p coredata.FindingPriority) string {
|
||||
switch p {
|
||||
case coredata.FindingPriorityLow:
|
||||
return "Low"
|
||||
case coredata.FindingPriorityMedium:
|
||||
return "Medium"
|
||||
case coredata.FindingPriorityHigh:
|
||||
return "High"
|
||||
default:
|
||||
return string(p)
|
||||
}
|
||||
}
|
||||
|
||||
var findingListTemplate = template.Must(
|
||||
template.New("finding_list.json.tmpl").
|
||||
Funcs(template.FuncMap{
|
||||
"json": func(v any) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
},
|
||||
}).
|
||||
ParseFS(Templates, "templates/finding_list.json.tmpl"),
|
||||
)
|
||||
|
||||
func BuildFindingListDocument(data docgen.FindingListData) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := findingListTemplate.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("cannot execute finding list template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (s *GeneratedDocumentService) PublishObligationList(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
approverIDs []gid.GID,
|
||||
) (*coredata.Document, *coredata.DocumentVersion, error) {
|
||||
var (
|
||||
document *coredata.Document
|
||||
documentVersion *coredata.DocumentVersion
|
||||
)
|
||||
|
||||
err := s.svc.pg.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
organization := &coredata.Organization{}
|
||||
if err := organization.LoadByID(ctx, tx, s.svc.scope, organizationID); err != nil {
|
||||
return fmt.Errorf("cannot load organization: %w", err)
|
||||
}
|
||||
|
||||
documentData, err := s.buildObligationListDocumentData(ctx, tx, organization)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build document data: %w", err)
|
||||
}
|
||||
|
||||
prosemirrorJSON, err := BuildObligationListDocument(documentData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build prosemirror document: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
obligation := coredata.Obligation{}
|
||||
obligationDocumentID, err := obligation.GetGeneratedDocumentID(ctx, tx, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query generated documents: %w", err)
|
||||
}
|
||||
|
||||
var existingDoc *coredata.Document
|
||||
if obligationDocumentID != nil {
|
||||
doc := &coredata.Document{}
|
||||
err = doc.LoadByID(ctx, tx, s.svc.scope, *obligationDocumentID)
|
||||
if err != nil && !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return fmt.Errorf("cannot load obligation list document: %w", err)
|
||||
}
|
||||
|
||||
if err == nil && doc.ArchivedAt == nil {
|
||||
existingDoc = doc
|
||||
} else {
|
||||
if err := obligation.ClearGeneratedDocumentID(ctx, tx, []gid.GID{*obligationDocumentID}); err != nil {
|
||||
return fmt.Errorf("cannot clear document reference: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasApprovers := len(approverIDs) > 0
|
||||
|
||||
if existingDoc == nil {
|
||||
documentID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentEntityType)
|
||||
|
||||
document = &coredata.Document{
|
||||
ID: documentID,
|
||||
OrganizationID: organizationID,
|
||||
WriteMode: coredata.DocumentWriteModeGenerated,
|
||||
TrustCenterVisibility: coredata.TrustCenterVisibilityNone,
|
||||
Status: coredata.DocumentStatusActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := document.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||
return fmt.Errorf("cannot insert document: %w", err)
|
||||
}
|
||||
|
||||
if err := obligation.UpsertGeneratedDocumentID(ctx, tx, organizationID, s.svc.scope.GetTenantID(), documentID); err != nil {
|
||||
return fmt.Errorf("cannot upsert generated documents: %w", err)
|
||||
}
|
||||
} else {
|
||||
document = existingDoc
|
||||
}
|
||||
|
||||
var newMajor int
|
||||
if document.CurrentPublishedMajor != nil {
|
||||
newMajor = *document.CurrentPublishedMajor + 1
|
||||
} else {
|
||||
newMajor = 1
|
||||
}
|
||||
|
||||
versionStatus := coredata.DocumentVersionStatusPublished
|
||||
var publishedAt *time.Time
|
||||
if hasApprovers {
|
||||
versionStatus = coredata.DocumentVersionStatusDraft
|
||||
} else {
|
||||
publishedAt = &now
|
||||
}
|
||||
|
||||
documentVersionID := gid.New(s.svc.scope.GetTenantID(), coredata.DocumentVersionEntityType)
|
||||
documentVersion = &coredata.DocumentVersion{
|
||||
ID: documentVersionID,
|
||||
OrganizationID: organizationID,
|
||||
DocumentID: document.ID,
|
||||
Title: "Obligation List",
|
||||
Major: newMajor,
|
||||
Minor: 0,
|
||||
Content: prosemirrorJSON,
|
||||
Status: versionStatus,
|
||||
Classification: coredata.DocumentClassificationConfidential,
|
||||
DocumentType: coredata.DocumentTypeRegister,
|
||||
Orientation: coredata.DocumentVersionOrientationLandscape,
|
||||
PublishedAt: publishedAt,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := documentVersion.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||
return fmt.Errorf("a version is pending approval, approve or reject it before publishing a new one: %w", err)
|
||||
}
|
||||
return fmt.Errorf("cannot insert document version: %w", err)
|
||||
}
|
||||
|
||||
if hasApprovers {
|
||||
defaultApprovers := &coredata.DocumentDefaultApprovers{}
|
||||
if err := defaultApprovers.MergeByDocumentID(ctx, tx, s.svc.scope, document.ID, organizationID, approverIDs); err != nil {
|
||||
return fmt.Errorf("cannot save default approvers: %w", err)
|
||||
}
|
||||
|
||||
_, err := s.svc.DocumentApprovals.RequestApprovalInTx(
|
||||
ctx,
|
||||
tx,
|
||||
document,
|
||||
documentVersion,
|
||||
approverIDs,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot request approval: %w", err)
|
||||
}
|
||||
} else {
|
||||
document.CurrentPublishedMajor = &newMajor
|
||||
document.CurrentPublishedMinor = new(0)
|
||||
document.UpdatedAt = now
|
||||
|
||||
if err := document.Update(ctx, tx, s.svc.scope); err != nil {
|
||||
return fmt.Errorf("cannot update document: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return document, documentVersion, nil
|
||||
}
|
||||
|
||||
func (s *GeneratedDocumentService) GetObligationsDocumentID(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
) (*gid.GID, error) {
|
||||
var obligationDocumentID *gid.GID
|
||||
|
||||
err := s.svc.pg.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||
obligation := coredata.Obligation{}
|
||||
var err error
|
||||
obligationDocumentID, err = obligation.GetGeneratedDocumentID(ctx, conn, organizationID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get obligation list document ID: %w", err)
|
||||
}
|
||||
|
||||
return obligationDocumentID, nil
|
||||
}
|
||||
|
||||
func (s *GeneratedDocumentService) buildObligationListDocumentData(
|
||||
ctx context.Context,
|
||||
conn pg.Querier,
|
||||
organization *coredata.Organization,
|
||||
) (docgen.ObligationListData, error) {
|
||||
var obligations coredata.Obligations
|
||||
if err := obligations.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organization.ID); err != nil {
|
||||
return docgen.ObligationListData{}, fmt.Errorf("cannot load obligations: %w", err)
|
||||
}
|
||||
|
||||
if len(obligations) == 0 {
|
||||
return docgen.ObligationListData{
|
||||
Title: "Obligation List",
|
||||
OrganizationName: organization.Name,
|
||||
CreatedAt: time.Now(),
|
||||
TotalObligations: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ownerIDs := make([]gid.GID, 0, len(obligations))
|
||||
ownerIDSet := make(map[gid.GID]struct{})
|
||||
for _, o := range obligations {
|
||||
if o.OwnerID == gid.Nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := ownerIDSet[o.OwnerID]; !ok {
|
||||
ownerIDs = append(ownerIDs, o.OwnerID)
|
||||
ownerIDSet[o.OwnerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
profileMap := make(map[gid.GID]*coredata.MembershipProfile)
|
||||
if len(ownerIDs) > 0 {
|
||||
var profiles coredata.MembershipProfiles
|
||||
if err := profiles.LoadByIDs(ctx, conn, s.svc.scope, ownerIDs); err != nil {
|
||||
return docgen.ObligationListData{}, fmt.Errorf("cannot load profiles: %w", err)
|
||||
}
|
||||
|
||||
for _, p := range profiles {
|
||||
profileMap[p.ID] = p
|
||||
}
|
||||
}
|
||||
|
||||
rows := make([]docgen.ObligationListRow, 0, len(obligations))
|
||||
for _, o := range obligations {
|
||||
ownerName := "-"
|
||||
if p, ok := profileMap[o.OwnerID]; ok {
|
||||
ownerName = p.FullName
|
||||
}
|
||||
|
||||
area := "-"
|
||||
if o.Area != nil && *o.Area != "" {
|
||||
area = *o.Area
|
||||
}
|
||||
|
||||
source := "-"
|
||||
if o.Source != nil && *o.Source != "" {
|
||||
source = *o.Source
|
||||
}
|
||||
|
||||
requirement := "-"
|
||||
if o.Requirement != nil && *o.Requirement != "" {
|
||||
requirement = *o.Requirement
|
||||
}
|
||||
|
||||
actionsToBeImplemented := "-"
|
||||
if o.ActionsToBeImplemented != nil && *o.ActionsToBeImplemented != "" {
|
||||
actionsToBeImplemented = *o.ActionsToBeImplemented
|
||||
}
|
||||
|
||||
regulator := "-"
|
||||
if o.Regulator != nil && *o.Regulator != "" {
|
||||
regulator = *o.Regulator
|
||||
}
|
||||
|
||||
dueDate := "-"
|
||||
if o.DueDate != nil {
|
||||
dueDate = o.DueDate.Format("2006-01-02")
|
||||
}
|
||||
|
||||
rows = append(rows, docgen.ObligationListRow{
|
||||
Area: area,
|
||||
Source: source,
|
||||
Requirement: requirement,
|
||||
ActionsToBeImplemented: actionsToBeImplemented,
|
||||
Status: formatObligationStatus(o.Status),
|
||||
Type: formatObligationType(o.Type),
|
||||
Regulator: regulator,
|
||||
Owner: ownerName,
|
||||
DueDate: dueDate,
|
||||
})
|
||||
}
|
||||
|
||||
return docgen.ObligationListData{
|
||||
Title: "Obligation List",
|
||||
OrganizationName: organization.Name,
|
||||
CreatedAt: time.Now(),
|
||||
TotalObligations: len(obligations),
|
||||
Rows: rows,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func formatObligationStatus(s coredata.ObligationStatus) string {
|
||||
switch s {
|
||||
case coredata.ObligationStatusNonCompliant:
|
||||
return "Non Compliant"
|
||||
case coredata.ObligationStatusPartiallyCompliant:
|
||||
return "Partially Compliant"
|
||||
case coredata.ObligationStatusCompliant:
|
||||
return "Compliant"
|
||||
default:
|
||||
return string(s)
|
||||
}
|
||||
}
|
||||
|
||||
func formatObligationType(t coredata.ObligationType) string {
|
||||
switch t {
|
||||
case coredata.ObligationTypeLegal:
|
||||
return "Legal"
|
||||
case coredata.ObligationTypeContractual:
|
||||
return "Contractual"
|
||||
default:
|
||||
return string(t)
|
||||
}
|
||||
}
|
||||
|
||||
var obligationListTemplate = template.Must(
|
||||
template.New("obligation_list.json.tmpl").
|
||||
Funcs(template.FuncMap{
|
||||
"json": func(v any) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
},
|
||||
}).
|
||||
ParseFS(Templates, "templates/obligation_list.json.tmpl"),
|
||||
)
|
||||
|
||||
func BuildObligationListDocument(data docgen.ObligationListData) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if err := obligationListTemplate.Execute(&buf, data); err != nil {
|
||||
return "", fmt.Errorf("cannot execute obligation list template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
@@ -289,7 +289,6 @@ func (s *ObligationService) Delete(
|
||||
func (s ObligationService) CountForOrganizationID(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
filter *coredata.ObligationFilter,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
@@ -297,7 +296,7 @@ func (s ObligationService) CountForOrganizationID(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
obligations := coredata.Obligations{}
|
||||
count, err = obligations.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter)
|
||||
count, err = obligations.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count obligations: %w", err)
|
||||
}
|
||||
@@ -317,7 +316,6 @@ 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{}
|
||||
@@ -329,7 +327,7 @@ func (s ObligationService) ListForControlID(
|
||||
return fmt.Errorf("cannot load control: %w", err)
|
||||
}
|
||||
|
||||
err := obligations.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor, filter)
|
||||
err := obligations.LoadByControlID(ctx, conn, s.svc.scope, control.ID, cursor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load obligations: %w", err)
|
||||
}
|
||||
@@ -349,14 +347,13 @@ func (s ObligationService) ListForOrganizationID(
|
||||
ctx context.Context,
|
||||
organizationID gid.GID,
|
||||
cursor *page.Cursor[coredata.ObligationOrderField],
|
||||
filter *coredata.ObligationFilter,
|
||||
) (*page.Page[*coredata.Obligation, coredata.ObligationOrderField], error) {
|
||||
var obligations coredata.Obligations
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
err := obligations.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor, filter)
|
||||
err := obligations.LoadByOrganizationID(ctx, conn, s.svc.scope, organizationID, cursor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load obligations: %w", err)
|
||||
}
|
||||
@@ -375,7 +372,6 @@ func (s ObligationService) ListForOrganizationID(
|
||||
func (s ObligationService) CountForRiskID(
|
||||
ctx context.Context,
|
||||
riskID gid.GID,
|
||||
filter *coredata.ObligationFilter,
|
||||
) (int, error) {
|
||||
var count int
|
||||
|
||||
@@ -383,7 +379,7 @@ func (s ObligationService) CountForRiskID(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||
obligations := &coredata.Obligations{}
|
||||
count, err = obligations.CountByRiskID(ctx, conn, s.svc.scope, riskID, filter)
|
||||
count, err = obligations.CountByRiskID(ctx, conn, s.svc.scope, riskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot count obligations: %w", err)
|
||||
}
|
||||
@@ -403,14 +399,13 @@ func (s ObligationService) ListForRiskID(
|
||||
ctx context.Context,
|
||||
riskID gid.GID,
|
||||
cursor *page.Cursor[coredata.ObligationOrderField],
|
||||
filter *coredata.ObligationFilter,
|
||||
) (*page.Page[*coredata.Obligation, coredata.ObligationOrderField], error) {
|
||||
var obligations coredata.Obligations
|
||||
|
||||
err := s.svc.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
err := obligations.LoadByRiskID(ctx, conn, s.svc.scope, riskID, cursor, filter)
|
||||
err := obligations.LoadByRiskID(ctx, conn, s.svc.scope, riskID, cursor)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load obligations: %w", err)
|
||||
}
|
||||
|
||||
@@ -171,10 +171,6 @@ var AuditorPolicy = policy.NewPolicy(
|
||||
ActionEmployeeDocumentGet, ActionEmployeeDocumentList,
|
||||
ActionEmployeeDocumentVersionExportPDF,
|
||||
).WithSID("employee-document-access").When(organizationCondition),
|
||||
|
||||
policy.Allow(
|
||||
ActionStatementOfApplicabilityPublish,
|
||||
).WithSID("soa-publish").When(organizationCondition),
|
||||
).WithDescription("Read-only probo access for auditors (excludes internal/employee content)")
|
||||
|
||||
// EmployeePolicy defines permissions for employee role.
|
||||
|
||||
101
pkg/probo/templates/finding_list.json.tmpl
Normal file
101
pkg/probo/templates/finding_list.json.tmpl
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "1. Purpose" }]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{ "type": "text", "text": "This document provides a comprehensive list of findings identified within the organization. It serves as a record of all findings, their classification, status, ownership, and remediation details." }]
|
||||
},
|
||||
{ "type": "horizontalRule" },
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "2. Finding List" }]
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"content": [
|
||||
{
|
||||
"type": "tableRow",
|
||||
"content": [
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [70] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Reference", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [90] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Kind", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [130] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Description", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Source", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [75] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Identified On", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [110] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Root Cause", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [110] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Corrective Action", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [110] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Effectiveness Check", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Status", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [60] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Priority", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [90] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Owner", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [75] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Due Date", "marks": [{ "type": "bold" }] }] }] }
|
||||
]
|
||||
}{{range .Rows}},
|
||||
{
|
||||
"type": "tableRow",
|
||||
"content": [
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [70] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .ReferenceID}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [90] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Kind}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [130] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Description}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Source}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [75] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .IdentifiedOn}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [110] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .RootCause}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [110] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .CorrectiveAction}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [110] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .EffectivenessCheck}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Status}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [60] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Priority}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [90] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Owner}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [75] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .DueDate}} }] }] }
|
||||
]
|
||||
}{{end}}
|
||||
]
|
||||
},
|
||||
{ "type": "horizontalRule" },
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "3. Definitions" }]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 3 },
|
||||
"content": [{ "type": "text", "text": "Kind" }]
|
||||
},
|
||||
{
|
||||
"type": "bulletList",
|
||||
"content": [
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Minor Nonconformity: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "A nonconformity that does not significantly affect the management system's ability to achieve its intended outcomes." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Major Nonconformity: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "A nonconformity that significantly affects the management system's ability to achieve its intended outcomes." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Observation: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "A noted issue that does not constitute a nonconformity but may warrant attention." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Exception: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "A deviation from a requirement that has been formally approved." }] }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 3 },
|
||||
"content": [{ "type": "text", "text": "Priority" }]
|
||||
},
|
||||
{
|
||||
"type": "bulletList",
|
||||
"content": [
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Low: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Finding has minimal impact and can be addressed in the normal course of operations." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Medium: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Finding has moderate impact and should be addressed in a timely manner." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "High: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Finding has significant impact and requires urgent attention." }] }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 3 },
|
||||
"content": [{ "type": "text", "text": "Owner" }]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{ "type": "text", "text": "The individual responsible for addressing the finding, including implementing corrective actions and tracking remediation progress." }]
|
||||
}
|
||||
]
|
||||
}
|
||||
93
pkg/probo/templates/obligation_list.json.tmpl
Normal file
93
pkg/probo/templates/obligation_list.json.tmpl
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "1. Purpose" }]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{ "type": "text", "text": "This document provides a comprehensive list of obligations managed by the organization. It serves as a record of all legal and contractual obligations, their compliance status, ownership, and regulatory details." }]
|
||||
},
|
||||
{ "type": "horizontalRule" },
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "2. Obligation List" }]
|
||||
},
|
||||
{
|
||||
"type": "table",
|
||||
"content": [
|
||||
{
|
||||
"type": "tableRow",
|
||||
"content": [
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Area", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Source", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [130] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Requirement", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [130] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Actions to be Implemented", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [70] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Type", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Status", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [70] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Regulator", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [90] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Owner", "marks": [{ "type": "bold" }] }] }] },
|
||||
{ "type": "tableHeader", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Due Date", "marks": [{ "type": "bold" }] }] }] }
|
||||
]
|
||||
}{{range .Rows}},
|
||||
{
|
||||
"type": "tableRow",
|
||||
"content": [
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Area}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Source}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [130] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Requirement}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [130] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .ActionsToBeImplemented}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [70] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Type}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Status}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [70] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Regulator}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [90] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .Owner}} }] }] },
|
||||
{ "type": "tableCell", "attrs": { "colspan": 1, "rowspan": 1, "colwidth": [80] }, "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": {{json .DueDate}} }] }] }
|
||||
]
|
||||
}{{end}}
|
||||
]
|
||||
},
|
||||
{ "type": "horizontalRule" },
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "3. Definitions" }]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 3 },
|
||||
"content": [{ "type": "text", "text": "Type" }]
|
||||
},
|
||||
{
|
||||
"type": "bulletList",
|
||||
"content": [
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Legal: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Obligations arising from laws, regulations, and statutory requirements applicable to the organization." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Contractual: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "Obligations arising from contracts, agreements, and other binding commitments with third parties." }] }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 3 },
|
||||
"content": [{ "type": "text", "text": "Status" }]
|
||||
},
|
||||
{
|
||||
"type": "bulletList",
|
||||
"content": [
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Non Compliant: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The organization does not meet the obligation requirements." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Partially Compliant: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The organization partially meets the obligation requirements but gaps remain." }] }] },
|
||||
{ "type": "listItem", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "Compliant: ", "marks": [{ "type": "bold" }] }, { "type": "text", "text": "The organization fully meets the obligation requirements." }] }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "heading",
|
||||
"attrs": { "level": 3 },
|
||||
"content": [{ "type": "text", "text": "Owner" }]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"content": [{ "type": "text", "text": "The individual responsible for ensuring the obligation is met, including monitoring compliance and coordinating necessary actions." }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -181,10 +181,7 @@ func (r *auditResolver) Findings(ctx context.Context, obj *types.Audit, first *i
|
||||
ownerID = filter.OwnerID
|
||||
}
|
||||
|
||||
findingFilter := coredata.NewFindingFilter(nil, kind, status, priority, ownerID)
|
||||
if filter != nil {
|
||||
findingFilter = coredata.NewFindingFilter(&filter.SnapshotID, kind, status, priority, ownerID)
|
||||
}
|
||||
findingFilter := coredata.NewFindingFilter(kind, status, priority, ownerID)
|
||||
|
||||
p, err := prb.Findings.ListForAuditID(ctx, obj.ID, cursor, findingFilter)
|
||||
if err != nil {
|
||||
@@ -363,10 +360,7 @@ func (r *findingConnectionResolver) TotalCount(ctx context.Context, obj *types.F
|
||||
ownerID = obj.Filter.OwnerID
|
||||
}
|
||||
|
||||
findingFilter := coredata.NewFindingFilter(nil, kind, status, priority, ownerID)
|
||||
if obj.Filter != nil {
|
||||
findingFilter = coredata.NewFindingFilter(&obj.Filter.SnapshotID, kind, status, priority, ownerID)
|
||||
}
|
||||
findingFilter := coredata.NewFindingFilter(kind, status, priority, ownerID)
|
||||
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
@@ -677,6 +671,29 @@ func (r *mutationResolver) DeleteFindingAuditMapping(ctx context.Context, input
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublishFindingList is the resolver for the publishFindingList field.
|
||||
func (r *mutationResolver) PublishFindingList(ctx context.Context, input types.PublishFindingListInput) (*types.PublishFindingListPayload, error) {
|
||||
if err := r.authorize(ctx, input.OrganizationID, probo.ActionFindingPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||
|
||||
document, documentVersion, err := prb.GeneratedDocuments.PublishFindingList(ctx, input.OrganizationID, input.ApproverIds)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||
return nil, gqlutils.Conflict(ctx, err)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot publish finding list", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.PublishFindingListPayload{
|
||||
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldCreatedAt),
|
||||
DocumentVersionEdge: types.NewDocumentVersionEdge(documentVersion, coredata.DocumentVersionOrderFieldCreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DownloadURL is the resolver for the downloadUrl field.
|
||||
func (r *reportResolver) DownloadURL(ctx context.Context, obj *types.Report) (*string, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionReportDownloadUrlGet); err != nil {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/vikstrous/dataloadgen"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/dataloader"
|
||||
@@ -272,7 +271,7 @@ func (r *controlResolver) Audits(ctx context.Context, obj *types.Control, first
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func (r *controlResolver) Obligations(ctx context.Context, obj *types.Control, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy) (*types.ObligationConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionObligationList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -292,18 +291,13 @@ func (r *controlResolver) Obligations(ctx context.Context, obj *types.Control, f
|
||||
|
||||
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)
|
||||
page, err := prb.Obligations.ListForControlID(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list control obligations", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewObligationConnection(page, r, obj.ID, filter), nil
|
||||
return types.NewObligationConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// Snapshots is the resolver for the snapshots field.
|
||||
|
||||
@@ -138,7 +138,6 @@ input FindingOrder {
|
||||
}
|
||||
|
||||
input FindingFilter {
|
||||
snapshotId: ID
|
||||
kind: FindingKind
|
||||
status: FindingStatus
|
||||
priority: FindingPriority
|
||||
@@ -171,7 +170,7 @@ type Audit implements Node {
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: FindingOrder
|
||||
filter: FindingFilter = { snapshotId: null }
|
||||
filter: FindingFilter
|
||||
): FindingConnection @goField(forceResolver: true)
|
||||
|
||||
trustCenterVisibility: TrustCenterVisibility!
|
||||
@@ -183,7 +182,6 @@ type Audit implements Node {
|
||||
|
||||
type Finding implements Node {
|
||||
id: ID!
|
||||
snapshotId: ID
|
||||
organization: Organization @goField(forceResolver: true)
|
||||
kind: FindingKind!
|
||||
referenceId: String!
|
||||
@@ -270,6 +268,19 @@ extend type Mutation {
|
||||
deleteFindingAuditMapping(
|
||||
input: DeleteFindingAuditMappingInput!
|
||||
): DeleteFindingAuditMappingPayload
|
||||
publishFindingList(
|
||||
input: PublishFindingListInput!
|
||||
): PublishFindingListPayload!
|
||||
}
|
||||
|
||||
input PublishFindingListInput {
|
||||
organizationId: ID!
|
||||
approverIds: [ID!]
|
||||
}
|
||||
|
||||
type PublishFindingListPayload {
|
||||
documentEdge: DocumentEdge!
|
||||
documentVersionEdge: DocumentVersionEdge!
|
||||
}
|
||||
|
||||
input CreateAuditInput {
|
||||
|
||||
@@ -139,7 +139,6 @@ type Control implements Node {
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: ObligationOrder
|
||||
filter: ObligationFilter
|
||||
): ObligationConnection! @goField(forceResolver: true)
|
||||
|
||||
snapshots(
|
||||
|
||||
@@ -51,14 +51,8 @@ input ObligationOrder
|
||||
field: ObligationOrderField!
|
||||
}
|
||||
|
||||
input ObligationFilter {
|
||||
snapshotId: ID
|
||||
}
|
||||
|
||||
type Obligation implements Node {
|
||||
id: ID!
|
||||
snapshotId: ID
|
||||
sourceId: ID
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
area: String
|
||||
source: String
|
||||
@@ -94,6 +88,19 @@ extend type Mutation {
|
||||
createObligation(input: CreateObligationInput!): CreateObligationPayload!
|
||||
updateObligation(input: UpdateObligationInput!): UpdateObligationPayload!
|
||||
deleteObligation(input: DeleteObligationInput!): DeleteObligationPayload!
|
||||
publishObligationList(
|
||||
input: PublishObligationListInput!
|
||||
): PublishObligationListPayload!
|
||||
}
|
||||
|
||||
input PublishObligationListInput {
|
||||
organizationId: ID!
|
||||
approverIds: [ID!]
|
||||
}
|
||||
|
||||
type PublishObligationListPayload {
|
||||
documentEdge: DocumentEdge!
|
||||
documentVersionEdge: DocumentVersionEdge!
|
||||
}
|
||||
|
||||
input CreateObligationInput {
|
||||
|
||||
@@ -150,13 +150,15 @@ type Organization implements Node {
|
||||
orderBy: AuditOrder
|
||||
): AuditConnection! @goField(forceResolver: true)
|
||||
|
||||
findingsDocument: Document @goField(forceResolver: true)
|
||||
|
||||
findings(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: FindingOrder
|
||||
filter: FindingFilter = { snapshotId: null }
|
||||
filter: FindingFilter
|
||||
): FindingConnection @goField(forceResolver: true)
|
||||
|
||||
auditLogEntries(
|
||||
@@ -247,13 +249,14 @@ type Organization implements Node {
|
||||
filter: MeasureFilter
|
||||
): MeasureConnection! @goField(forceResolver: true)
|
||||
|
||||
obligationsDocument: Document @goField(forceResolver: true)
|
||||
|
||||
obligations(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: ObligationOrder
|
||||
filter: ObligationFilter = { snapshotId: null }
|
||||
): ObligationConnection! @goField(forceResolver: true)
|
||||
|
||||
processingActivities(
|
||||
|
||||
@@ -107,7 +107,6 @@ type Risk implements Node {
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: ObligationOrder
|
||||
filter: ObligationFilter
|
||||
): ObligationConnection! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
|
||||
@@ -3,14 +3,6 @@ enum SnapshotsType
|
||||
RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
|
||||
VENDORS
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors")
|
||||
FINDINGS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeFindings"
|
||||
)
|
||||
OBLIGATIONS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeObligations"
|
||||
)
|
||||
PROCESSING_ACTIVITIES
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeProcessingActivities"
|
||||
|
||||
@@ -112,6 +112,29 @@ func (r *mutationResolver) DeleteObligation(ctx context.Context, input types.Del
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublishObligationList is the resolver for the publishObligationList field.
|
||||
func (r *mutationResolver) PublishObligationList(ctx context.Context, input types.PublishObligationListInput) (*types.PublishObligationListPayload, error) {
|
||||
if err := r.authorize(ctx, input.OrganizationID, probo.ActionObligationPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||
|
||||
document, documentVersion, err := prb.GeneratedDocuments.PublishObligationList(ctx, input.OrganizationID, input.ApproverIds)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||
return nil, gqlutils.Conflict(ctx, err)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot publish obligation list", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.PublishObligationListPayload{
|
||||
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldCreatedAt),
|
||||
DocumentVersionEdge: types.NewDocumentVersionEdge(documentVersion, coredata.DocumentVersionOrderFieldCreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *obligationResolver) Organization(ctx context.Context, obj *types.Obligation) (*types.Organization, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionOrganizationGet); err != nil {
|
||||
@@ -169,24 +192,14 @@ func (r *obligationConnectionResolver) TotalCount(ctx context.Context, obj *type
|
||||
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
obligationFilter := coredata.NewObligationFilter(nil)
|
||||
if obj.Filter != nil {
|
||||
obligationFilter = coredata.NewObligationFilter(&obj.Filter.SnapshotID)
|
||||
}
|
||||
|
||||
count, err := prb.Obligations.CountForOrganizationID(ctx, obj.ParentID, obligationFilter)
|
||||
count, err := prb.Obligations.CountForOrganizationID(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count obligations", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
}
|
||||
return count, nil
|
||||
case *riskResolver:
|
||||
obligationFilter := coredata.NewObligationFilter(nil)
|
||||
if obj.Filter != nil {
|
||||
obligationFilter = coredata.NewObligationFilter(&obj.Filter.SnapshotID)
|
||||
}
|
||||
|
||||
count, err := prb.Obligations.CountForRiskID(ctx, obj.ParentID, obligationFilter)
|
||||
count, err := prb.Obligations.CountForRiskID(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count risk obligations", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
|
||||
@@ -362,6 +362,30 @@ func (r *organizationResolver) Audits(ctx context.Context, obj *types.Organizati
|
||||
return types.NewAuditConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// FindingsDocument is the resolver for the findingsDocument field.
|
||||
func (r *organizationResolver) FindingsDocument(ctx context.Context, obj *types.Organization) (*types.Document, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||
|
||||
findingDocumentID, err := prb.GeneratedDocuments.GetFindingsDocumentID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get finding list document ID: %w", err)
|
||||
}
|
||||
if findingDocumentID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
doc, err := prb.Documents.Get(ctx, *findingDocumentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get finding list document: %w", err)
|
||||
}
|
||||
|
||||
return types.NewDocument(doc), nil
|
||||
}
|
||||
|
||||
// Findings is the resolver for the findings field.
|
||||
func (r *organizationResolver) Findings(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.FindingOrder, filter *types.FindingFilter) (*types.FindingConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionFindingList); err != nil {
|
||||
@@ -397,10 +421,7 @@ func (r *organizationResolver) Findings(ctx context.Context, obj *types.Organiza
|
||||
ownerID = filter.OwnerID
|
||||
}
|
||||
|
||||
findingFilter := coredata.NewFindingFilter(nil, kind, status, priority, ownerID)
|
||||
if filter != nil {
|
||||
findingFilter = coredata.NewFindingFilter(&filter.SnapshotID, kind, status, priority, ownerID)
|
||||
}
|
||||
findingFilter := coredata.NewFindingFilter(kind, status, priority, ownerID)
|
||||
|
||||
page, err := prb.Findings.ListForOrganizationID(ctx, obj.ID, cursor, findingFilter)
|
||||
if err != nil {
|
||||
@@ -791,8 +812,32 @@ func (r *organizationResolver) Measures(ctx context.Context, obj *types.Organiza
|
||||
return types.NewMeasureConnection(page, r, obj.ID, measureFilter), nil
|
||||
}
|
||||
|
||||
// ObligationsDocument is the resolver for the obligationsDocument field.
|
||||
func (r *organizationResolver) ObligationsDocument(ctx context.Context, obj *types.Organization) (*types.Document, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||
|
||||
obligationDocumentID, err := prb.GeneratedDocuments.GetObligationsDocumentID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get obligation list document ID: %w", err)
|
||||
}
|
||||
if obligationDocumentID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
doc, err := prb.Documents.Get(ctx, *obligationDocumentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get obligation list document: %w", err)
|
||||
}
|
||||
|
||||
return types.NewDocument(doc), nil
|
||||
}
|
||||
|
||||
// Obligations is the resolver for the obligations field.
|
||||
func (r *organizationResolver) Obligations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy, filter *types.ObligationFilter) (*types.ObligationConnection, error) {
|
||||
func (r *organizationResolver) Obligations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy) (*types.ObligationConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionObligationList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -813,18 +858,13 @@ func (r *organizationResolver) Obligations(ctx context.Context, obj *types.Organ
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
obligationFilter := coredata.NewObligationFilter(nil)
|
||||
if filter != nil {
|
||||
obligationFilter = coredata.NewObligationFilter(&filter.SnapshotID)
|
||||
}
|
||||
|
||||
page, err := prb.Obligations.ListForOrganizationID(ctx, obj.ID, cursor, obligationFilter)
|
||||
page, err := prb.Obligations.ListForOrganizationID(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list organization obligations", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewObligationConnection(page, r, obj.ID, filter), nil
|
||||
return types.NewObligationConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// ProcessingActivities is the resolver for the processingActivities field.
|
||||
|
||||
@@ -393,7 +393,7 @@ func (r *riskResolver) Controls(ctx context.Context, obj *types.Risk, first *int
|
||||
}
|
||||
|
||||
// Obligations is the resolver for the obligations field.
|
||||
func (r *riskResolver) Obligations(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy, filter *types.ObligationFilter) (*types.ObligationConnection, error) {
|
||||
func (r *riskResolver) Obligations(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ObligationOrderBy) (*types.ObligationConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionObligationList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -413,18 +413,13 @@ func (r *riskResolver) Obligations(ctx context.Context, obj *types.Risk, first *
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
var obligationFilter = coredata.NewObligationFilter(nil)
|
||||
if filter != nil {
|
||||
obligationFilter = coredata.NewObligationFilter(&filter.SnapshotID)
|
||||
}
|
||||
|
||||
page, err := prb.Obligations.ListForRiskID(ctx, obj.ID, cursor, obligationFilter)
|
||||
page, err := prb.Obligations.ListForRiskID(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list risk obligations", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewObligationConnection(page, r, obj.ID, filter), nil
|
||||
return types.NewObligationConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
|
||||
@@ -67,8 +67,7 @@ func NewFindingEdge(f *coredata.Finding, orderField coredata.FindingOrderField)
|
||||
|
||||
func NewFinding(f *coredata.Finding) *Finding {
|
||||
finding := &Finding{
|
||||
ID: f.ID,
|
||||
SnapshotID: f.SnapshotID,
|
||||
ID: f.ID,
|
||||
Organization: &Organization{
|
||||
ID: f.OrganizationID,
|
||||
},
|
||||
|
||||
@@ -30,7 +30,6 @@ type (
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
Filter *ObligationFilter
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,7 +37,6 @@ func NewObligationConnection(
|
||||
p *page.Page[*coredata.Obligation, coredata.ObligationOrderField],
|
||||
parentType any,
|
||||
parentID gid.GID,
|
||||
filter *ObligationFilter,
|
||||
) *ObligationConnection {
|
||||
edges := make([]*ObligationEdge, len(p.Data))
|
||||
for i, obligation := range p.Data {
|
||||
@@ -51,15 +49,12 @@ func NewObligationConnection(
|
||||
|
||||
Resolver: parentType,
|
||||
ParentID: parentID,
|
||||
Filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
func NewObligation(cr *coredata.Obligation) *Obligation {
|
||||
return &Obligation{
|
||||
ID: cr.ID,
|
||||
SnapshotID: cr.SnapshotID,
|
||||
SourceID: cr.SourceID,
|
||||
ID: cr.ID,
|
||||
Organization: &Organization{
|
||||
ID: cr.OrganizationID,
|
||||
},
|
||||
|
||||
@@ -741,11 +741,9 @@ func (r *Resolver) ListFindingsTool(ctx context.Context, req *mcp.CallToolReques
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
noSnapshot := (*gid.GID)(nil)
|
||||
findingFilter := coredata.NewFindingFilter(&noSnapshot, nil, nil, nil, nil)
|
||||
findingFilter := coredata.NewFindingFilter(nil, nil, nil, nil)
|
||||
if input.Filter != nil {
|
||||
findingFilter = coredata.NewFindingFilter(
|
||||
&input.Filter.SnapshotID,
|
||||
input.Filter.Kind,
|
||||
input.Filter.Status,
|
||||
input.Filter.Priority,
|
||||
@@ -857,13 +855,7 @@ func (r *Resolver) ListObligationsTool(ctx context.Context, req *mcp.CallToolReq
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
noSnapshot := (*gid.GID)(nil)
|
||||
obligationFilter := coredata.NewObligationFilter(&noSnapshot)
|
||||
if input.Filter != nil {
|
||||
obligationFilter = coredata.NewObligationFilter(&input.Filter.SnapshotID)
|
||||
}
|
||||
|
||||
page, err := prb.Obligations.ListForOrganizationID(ctx, input.OrganizationID, cursor, obligationFilter)
|
||||
page, err := prb.Obligations.ListForOrganizationID(ctx, input.OrganizationID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list organization obligations: %w", err))
|
||||
}
|
||||
@@ -1610,7 +1602,7 @@ func (r *Resolver) ListControlObligationsTool(ctx context.Context, req *mcp.Call
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
obligationPage, err := prb.Obligations.ListForControlID(ctx, input.ControlID, cursor, coredata.NewObligationFilter(nil))
|
||||
obligationPage, err := prb.Obligations.ListForControlID(ctx, input.ControlID, cursor)
|
||||
if err != nil {
|
||||
return nil, types.ListControlObligationsOutput{}, fmt.Errorf("failed to list control obligations: %w", err)
|
||||
}
|
||||
@@ -1740,7 +1732,7 @@ func (r *Resolver) ListRiskObligationsTool(ctx context.Context, req *mcp.CallToo
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
obligationPage, err := prb.Obligations.ListForRiskID(ctx, input.RiskID, cursor, coredata.NewObligationFilter(nil))
|
||||
obligationPage, err := prb.Obligations.ListForRiskID(ctx, input.RiskID, cursor)
|
||||
if err != nil {
|
||||
return nil, types.ListRiskObligationsOutput{}, fmt.Errorf("failed to list risk obligations: %w", err)
|
||||
}
|
||||
@@ -4786,3 +4778,35 @@ func (r *Resolver) AssessVendorTool(ctx context.Context, req *mcp.CallToolReques
|
||||
|
||||
return nil, types.NewAssessVendorOutput(result), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishFindingListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishFindingListInput) (*mcp.CallToolResult, types.PublishFindingListOutput, error) {
|
||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionFindingPublish)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
document, documentVersion, err := svc.GeneratedDocuments.PublishFindingList(ctx, input.OrganizationID, input.ApproverIds)
|
||||
if err != nil {
|
||||
return nil, types.PublishFindingListOutput{}, fmt.Errorf("cannot publish finding list: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.PublishFindingListOutput{
|
||||
DocumentID: document.ID,
|
||||
DocumentVersionID: documentVersion.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishObligationListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishObligationListInput) (*mcp.CallToolResult, types.PublishObligationListOutput, error) {
|
||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionObligationPublish)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
document, documentVersion, err := svc.GeneratedDocuments.PublishObligationList(ctx, input.OrganizationID, input.ApproverIds)
|
||||
if err != nil {
|
||||
return nil, types.PublishObligationListOutput{}, fmt.Errorf("cannot publish obligation list: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.PublishObligationListOutput{
|
||||
DocumentID: document.ID,
|
||||
DocumentVersionID: documentVersion.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2827,18 +2827,6 @@ components:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
snapshot_id:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/GID"
|
||||
description: Snapshot ID
|
||||
- type: "null"
|
||||
description: No snapshot
|
||||
description: Snapshot ID
|
||||
source_id:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
description: Source ID
|
||||
kind:
|
||||
$ref: "#/components/schemas/FindingKind"
|
||||
description: Finding kind
|
||||
@@ -2929,12 +2917,6 @@ components:
|
||||
filter:
|
||||
type: object
|
||||
properties:
|
||||
snapshot_id:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/GID"
|
||||
- type: "null"
|
||||
description: Filter by snapshot ID. Defaults to null, which returns only findings with no snapshot (current live data). Pass a specific snapshot ID to retrieve findings as they were at that snapshot.
|
||||
default: null
|
||||
kind:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/FindingKind"
|
||||
@@ -3279,18 +3261,6 @@ components:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
snapshot_id:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/GID"
|
||||
description: Snapshot ID
|
||||
- type: "null"
|
||||
description: No snapshot
|
||||
description: Snapshot ID
|
||||
source_id:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
description: Source ID
|
||||
area:
|
||||
type:
|
||||
- string
|
||||
@@ -3363,16 +3333,6 @@ components:
|
||||
cursor:
|
||||
$ref: "#/components/schemas/CursorKey"
|
||||
description: Page cursor
|
||||
filter:
|
||||
type: object
|
||||
properties:
|
||||
snapshot_id:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/GID"
|
||||
- type: "null"
|
||||
description: Filter by snapshot ID. Defaults to null, which returns only obligations with no snapshot (current live data). Pass a specific snapshot ID to retrieve obligations as they were at that snapshot.
|
||||
default: null
|
||||
|
||||
ListObligationsOutput:
|
||||
type: object
|
||||
required:
|
||||
@@ -7051,6 +7011,60 @@ components:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created document version ID
|
||||
|
||||
PublishFindingListInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
approver_ids:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
|
||||
|
||||
PublishFindingListOutput:
|
||||
type: object
|
||||
required:
|
||||
- document_id
|
||||
- document_version_id
|
||||
properties:
|
||||
document_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created or updated document ID
|
||||
document_version_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created document version ID
|
||||
|
||||
PublishObligationListInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
approver_ids:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
|
||||
|
||||
PublishObligationListOutput:
|
||||
type: object
|
||||
required:
|
||||
- document_id
|
||||
- document_version_id
|
||||
properties:
|
||||
document_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created or updated document ID
|
||||
document_version_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created document version ID
|
||||
|
||||
PublishStatementOfApplicabilityInput:
|
||||
type: object
|
||||
required:
|
||||
@@ -10288,6 +10302,22 @@ tools:
|
||||
$ref: "#/components/schemas/PublishAssetListInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishAssetListOutput"
|
||||
- name: publishFindingList
|
||||
description: Publish the finding register for an organization as a document. If a document already exists, a new version is created.
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/PublishFindingListInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishFindingListOutput"
|
||||
- name: publishObligationList
|
||||
description: Publish the obligation register for an organization as a document. If a document already exists, a new version is created.
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/PublishObligationListInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishObligationListOutput"
|
||||
- name: publishStatementOfApplicability
|
||||
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
|
||||
hints:
|
||||
|
||||
@@ -23,7 +23,6 @@ func NewFinding(f *coredata.Finding) *Finding {
|
||||
finding := &Finding{
|
||||
ID: f.ID,
|
||||
OrganizationID: f.OrganizationID,
|
||||
SnapshotID: f.SnapshotID,
|
||||
Kind: f.Kind,
|
||||
ReferenceID: f.ReferenceID,
|
||||
Description: f.Description,
|
||||
@@ -41,11 +40,6 @@ func NewFinding(f *coredata.Finding) *Finding {
|
||||
UpdatedAt: f.UpdatedAt,
|
||||
}
|
||||
|
||||
if f.SourceID != nil {
|
||||
s := f.SourceID.String()
|
||||
finding.SourceID = &s
|
||||
}
|
||||
|
||||
return finding
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ func NewObligation(o *coredata.Obligation) *Obligation {
|
||||
obligation := &Obligation{
|
||||
ID: o.ID,
|
||||
OrganizationID: o.OrganizationID,
|
||||
SnapshotID: o.SnapshotID,
|
||||
Area: o.Area,
|
||||
Source: o.Source,
|
||||
Requirement: o.Requirement,
|
||||
@@ -38,11 +37,6 @@ func NewObligation(o *coredata.Obligation) *Obligation {
|
||||
UpdatedAt: o.UpdatedAt,
|
||||
}
|
||||
|
||||
if o.SourceID != nil {
|
||||
s := o.SourceID.String()
|
||||
obligation.SourceID = &s
|
||||
}
|
||||
|
||||
return obligation
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user