diff --git a/apps/console/src/hooks/graph/FrameworkGraph.ts b/apps/console/src/hooks/graph/FrameworkGraph.ts
index fed43088e..c384cd5cd 100644
--- a/apps/console/src/hooks/graph/FrameworkGraph.ts
+++ b/apps/console/src/hooks/graph/FrameworkGraph.ts
@@ -103,6 +103,9 @@ export const frameworkControlNodeQuery = graphql`
name
sectionTitle
description
+ bestPractice
+ implemented
+ notImplementedJustification
canUpdate: permission(action: "core:control:update")
canDelete: permission(action: "core:control:delete")
canCreateMeasureMapping: permission(
diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx
index 186a73c5b..88021e882 100644
--- a/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx
+++ b/apps/console/src/pages/organizations/frameworks/FrameworkControlPage.tsx
@@ -3,7 +3,9 @@ import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
+ Badge,
Button,
+ Card,
DropdownItem,
IconPencil,
IconTrashCan,
@@ -354,6 +356,28 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{control.description}
)}
+
| {__("Framework")} | -{__("Control")} | -- {__("Applicability")} - | -{__("Justification")} | -- {__("Regulatory")} - | -- {__("Contractual")} - | -- {__("Best Practice")} - | -- {__("Risk Assessment")} - | +{__("Framework")} | +{__("Control")} | +{__("Applicability")} | +{__("Implemented")} | +{__("Regulatory")} | +{__("Contractual")} | +{__("Best Practice")} | +{__("Risk Assessment")} | {(canUpdate || canDelete) && ( -+ | )} |
-
-
+
+
{control.sectionTitle}
-
+
{control.name}
|
-
+
{control.applicability !== null
? (
- |
-
-
- {control.justification || (
-
- -
-
+ {control.justification && (
+
+ {control.justification} + )} |
-
- {control.applicability === false
- ? -
- : (
+ {control.applicability === false
+ ? -
+ : (
+
+ )}
+ {control.notImplementedJustification} + + )} + |
-
- {control.applicability === false
- ? -
- : (
-
+ {control.applicability === false
+ ? -
+ : control.regulatory
+ ? |
-
- {control.applicability === false
- ? -
- : (
-
+ {control.applicability === false
+ ? -
+ : control.contractual
+ ? |
-
- {control.applicability === false
- ? -
- : (
-
+ {control.applicability === false
+ ? -
+ : control.bestPractice
+ ? |
+
+ {control.applicability === false
+ ? -
+ : control.riskAssessment
+ ? |
{(canUpdate || canDelete) && (
diff --git a/e2e/console/control_test.go b/e2e/console/control_test.go
index 9ce80e456..611b237fb 100644
--- a/e2e/console/control_test.go
+++ b/e2e/console/control_test.go
@@ -64,6 +64,7 @@ func TestControl_Create(t *testing.T) {
"name": "Information Security Policies",
"description": "Policies for information security",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &result)
require.NoError(t, err)
@@ -269,6 +270,7 @@ func TestControl_RequiredFields(t *testing.T) {
"description": "Test",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
},
wantError: true,
@@ -281,6 +283,7 @@ func TestControl_RequiredFields(t *testing.T) {
"description": "Test",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
},
wantError: true,
@@ -293,6 +296,7 @@ func TestControl_RequiredFields(t *testing.T) {
"name": "Test Control",
"description": "Test",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
},
wantError: true,
@@ -305,6 +309,7 @@ func TestControl_RequiredFields(t *testing.T) {
"name": "Test Control",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
},
wantError: true,
@@ -317,6 +322,20 @@ func TestControl_RequiredFields(t *testing.T) {
"name": "Test Control",
"description": "Test",
"sectionTitle": "Section 1",
+ "implemented": "IMPLEMENTED",
+ },
+ },
+ wantError: true,
+ },
+ {
+ name: "Missing implemented should fail",
+ variables: map[string]any{
+ "input": map[string]any{
+ "frameworkId": frameworkID,
+ "name": "Test Control",
+ "description": "Test",
+ "sectionTitle": "Section 1",
+ "bestPractice": true,
},
},
wantError: true,
@@ -404,6 +423,7 @@ func TestControl_OmittableDescription(t *testing.T) {
"description": "Initial description",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &createResult)
require.NoError(t, err)
@@ -568,6 +588,7 @@ func TestControl_SubResolvers(t *testing.T) {
"description": "Test description",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &controlResult)
require.NoError(t, err)
diff --git a/e2e/console/mapping_test.go b/e2e/console/mapping_test.go
index db0c25969..b3486252a 100644
--- a/e2e/console/mapping_test.go
+++ b/e2e/console/mapping_test.go
@@ -83,6 +83,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
"description": "Test control for mapping",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &createControlResult)
require.NoError(t, err)
@@ -362,6 +363,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
"description": "Test control",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &createControlResult)
require.NoError(t, err)
@@ -503,6 +505,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
"description": "Test control",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &createControlResult)
require.NoError(t, err)
@@ -640,6 +643,7 @@ func TestControlSnapshotMapping_CreateDelete(t *testing.T) {
"description": "Test control",
"sectionTitle": "Section 1",
"bestPractice": true,
+ "implemented": "IMPLEMENTED",
},
}, &createControlResult)
require.NoError(t, err)
diff --git a/e2e/console/rbac_test.go b/e2e/console/rbac_test.go
index b1fdbf8fc..c2b58234d 100644
--- a/e2e/console/rbac_test.go
+++ b/e2e/console/rbac_test.go
@@ -384,7 +384,7 @@ func TestRBAC(t *testing.T) {
client: owner,
query: createControlMutation,
variables: func() map[string]any {
- return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Owner"), "bestPractice": true}}
+ return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Owner"), "bestPractice": true, "implemented": "IMPLEMENTED"}}
},
shouldAllow: true,
},
@@ -394,7 +394,7 @@ func TestRBAC(t *testing.T) {
client: admin,
query: createControlMutation,
variables: func() map[string]any {
- return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Admin"), "bestPractice": true}}
+ return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Admin"), "bestPractice": true, "implemented": "IMPLEMENTED"}}
},
shouldAllow: true,
},
@@ -404,7 +404,7 @@ func TestRBAC(t *testing.T) {
client: viewer,
query: createControlMutation,
variables: func() map[string]any {
- return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Viewer"), "bestPractice": true}}
+ return map[string]any{"input": map[string]any{"frameworkId": frameworkID, "name": factory.SafeName("Control"), "description": "Test", "sectionTitle": factory.SafeName("Section Viewer"), "bestPractice": true, "implemented": "IMPLEMENTED"}}
},
shouldAllow: false,
},
diff --git a/e2e/internal/factory/factory.go b/e2e/internal/factory/factory.go
index d9de38bce..6bcc3eab1 100644
--- a/e2e/internal/factory/factory.go
+++ b/e2e/internal/factory/factory.go
@@ -252,6 +252,11 @@ func CreateControl(c *testutil.Client, frameworkID string, attrs ...Attrs) strin
"description": a.getString("description", "Test control description"),
"sectionTitle": a.getString("sectionTitle", fmt.Sprintf("Section %s", gofakeit.LetterN(3))),
"bestPractice": a.getBool("bestPractice", true),
+ "implemented": a.getString("implemented", "IMPLEMENTED"),
+ }
+
+ if justification := a.getStringPtr("notImplementedJustification"); justification != nil {
+ input["notImplementedJustification"] = *justification
}
var result struct {
@@ -495,6 +500,16 @@ func (b *ControlBuilder) WithBestPractice(bestPractice bool) *ControlBuilder {
return b
}
+func (b *ControlBuilder) WithImplemented(implemented string) *ControlBuilder {
+ b.attrs["implemented"] = implemented
+ return b
+}
+
+func (b *ControlBuilder) WithNotImplementedJustification(justification string) *ControlBuilder {
+ b.attrs["notImplementedJustification"] = justification
+ return b
+}
+
func (b *ControlBuilder) Create() string {
return CreateControl(b.client, b.frameworkID, b.attrs)
}
diff --git a/pkg/cmd/control/create/create.go b/pkg/cmd/control/create/create.go
index b11305ee6..bf6b93d3d 100644
--- a/pkg/cmd/control/create/create.go
+++ b/pkg/cmd/control/create/create.go
@@ -33,6 +33,8 @@ mutation($input: CreateControlInput!) {
name
description
bestPractice
+ implemented
+ notImplementedJustification
}
}
}
@@ -43,11 +45,13 @@ type createResponse struct {
CreateControl struct {
ControlEdge struct {
Node struct {
- ID string `json:"id"`
- SectionTitle string `json:"sectionTitle"`
- Name string `json:"name"`
- Description *string `json:"description"`
- BestPractice bool `json:"bestPractice"`
+ ID string `json:"id"`
+ SectionTitle string `json:"sectionTitle"`
+ Name string `json:"name"`
+ Description *string `json:"description"`
+ BestPractice bool `json:"bestPractice"`
+ Implemented string `json:"implemented"`
+ NotImplementedJustification *string `json:"notImplementedJustification"`
} `json:"node"`
} `json:"controlEdge"`
} `json:"createControl"`
@@ -55,11 +59,13 @@ type createResponse struct {
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
var (
- flagFramework string
- flagSectionTitle string
- flagName string
- flagDescription string
- flagBestPractice bool
+ flagFramework string
+ flagSectionTitle string
+ flagName string
+ flagDescription string
+ flagBestPractice bool
+ flagNotImplemented bool
+ flagNotImplementedJustification string
)
cmd := &cobra.Command{
@@ -86,17 +92,27 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
cfg.HTTPTimeoutDuration(),
)
+ implemented := "IMPLEMENTED"
+ if flagNotImplemented {
+ implemented = "NOT_IMPLEMENTED"
+ }
+
input := map[string]any{
"frameworkId": flagFramework,
"sectionTitle": flagSectionTitle,
"name": flagName,
"bestPractice": flagBestPractice,
+ "implemented": implemented,
}
if flagDescription != "" {
input["description"] = flagDescription
}
+ if flagNotImplemented && flagNotImplementedJustification != "" {
+ input["notImplementedJustification"] = flagNotImplementedJustification
+ }
+
data, err := client.Do(
createMutation,
map[string]any{"input": input},
@@ -127,6 +143,8 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVar(&flagName, "name", "", "Control name (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Control description")
cmd.Flags().BoolVar(&flagBestPractice, "best-practice", false, "Mark as best practice")
+ cmd.Flags().BoolVar(&flagNotImplemented, "not-implemented", false, "Mark as not implemented")
+ cmd.Flags().StringVar(&flagNotImplementedJustification, "not-implemented-justification", "", "Justification for non-implementation")
_ = cmd.MarkFlagRequired("framework")
_ = cmd.MarkFlagRequired("section-title")
diff --git a/pkg/cmd/control/update/update.go b/pkg/cmd/control/update/update.go
index 4e6b020fb..9ffbf7d90 100644
--- a/pkg/cmd/control/update/update.go
+++ b/pkg/cmd/control/update/update.go
@@ -32,6 +32,8 @@ mutation($input: UpdateControlInput!) {
name
description
bestPractice
+ implemented
+ notImplementedJustification
}
}
}
@@ -40,21 +42,25 @@ mutation($input: UpdateControlInput!) {
type updateResponse struct {
UpdateControl struct {
Control struct {
- ID string `json:"id"`
- SectionTitle string `json:"sectionTitle"`
- Name string `json:"name"`
- Description *string `json:"description"`
- BestPractice bool `json:"bestPractice"`
+ ID string `json:"id"`
+ SectionTitle string `json:"sectionTitle"`
+ Name string `json:"name"`
+ Description *string `json:"description"`
+ BestPractice bool `json:"bestPractice"`
+ Implemented string `json:"implemented"`
+ NotImplementedJustification *string `json:"notImplementedJustification"`
} `json:"control"`
} `json:"updateControl"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
- flagSectionTitle string
- flagName string
- flagDescription string
- flagBestPractice bool
+ flagSectionTitle string
+ flagName string
+ flagDescription string
+ flagBestPractice bool
+ flagNotImplemented bool
+ flagNotImplementedJustification string
)
cmd := &cobra.Command{
@@ -99,6 +105,20 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
if cmd.Flags().Changed("best-practice") {
input["bestPractice"] = flagBestPractice
}
+ if cmd.Flags().Changed("not-implemented") {
+ if flagNotImplemented {
+ input["implemented"] = "NOT_IMPLEMENTED"
+ } else {
+ input["implemented"] = "IMPLEMENTED"
+ }
+ }
+ if cmd.Flags().Changed("not-implemented-justification") {
+ if flagNotImplementedJustification == "" {
+ input["notImplementedJustification"] = nil
+ } else {
+ input["notImplementedJustification"] = flagNotImplementedJustification
+ }
+ }
if len(input) == 1 {
return fmt.Errorf("at least one field must be specified for update")
@@ -133,6 +153,8 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
cmd.Flags().StringVar(&flagName, "name", "", "Control name")
cmd.Flags().StringVar(&flagDescription, "description", "", "Control description")
cmd.Flags().BoolVar(&flagBestPractice, "best-practice", false, "Mark as best practice")
+ cmd.Flags().BoolVar(&flagNotImplemented, "not-implemented", false, "Mark as not implemented")
+ cmd.Flags().StringVar(&flagNotImplementedJustification, "not-implemented-justification", "", "Justification for non-implementation")
return cmd
}
diff --git a/pkg/coredata/control.go b/pkg/coredata/control.go
index f130bd381..82d305cb2 100644
--- a/pkg/coredata/control.go
+++ b/pkg/coredata/control.go
@@ -30,15 +30,17 @@ import (
type (
Control struct {
- ID gid.GID `db:"id"`
- OrganizationID gid.GID `db:"organization_id"`
- SectionTitle string `db:"section_title"`
- FrameworkID gid.GID `db:"framework_id"`
- Name string `db:"name"`
- Description *string `db:"description"`
- BestPractice bool `db:"best_practice"`
- CreatedAt time.Time `db:"created_at"`
- UpdatedAt time.Time `db:"updated_at"`
+ ID gid.GID `db:"id"`
+ OrganizationID gid.GID `db:"organization_id"`
+ SectionTitle string `db:"section_title"`
+ FrameworkID gid.GID `db:"framework_id"`
+ Name string `db:"name"`
+ Description *string `db:"description"`
+ BestPractice bool `db:"best_practice"`
+ Implemented ControlImplementationState `db:"implemented"`
+ NotImplementedJustification *string `db:"not_implemented_justification"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
}
Controls []*Control
@@ -131,6 +133,8 @@ WITH ctrl AS (
c.name,
c.description,
c.best_practice,
+ c.implemented,
+ c.not_implemented_justification,
c.created_at,
c.updated_at,
c.search_vector
@@ -149,6 +153,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -240,6 +246,8 @@ WITH ctrl AS (
c.name,
c.description,
c.best_practice,
+ c.implemented,
+ c.not_implemented_justification,
c.created_at,
c.updated_at,
c.search_vector
@@ -258,6 +266,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -355,6 +365,8 @@ WITH ctrl AS (
c.name,
c.description,
c.best_practice,
+ c.implemented,
+ c.not_implemented_justification,
c.created_at,
c.updated_at,
c.search_vector
@@ -379,6 +391,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -458,6 +472,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -552,6 +568,8 @@ WITH ctrl AS (
c.name,
c.description,
c.best_practice,
+ c.implemented,
+ c.not_implemented_justification,
c.created_at,
c.updated_at,
c.search_vector
@@ -570,6 +588,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -616,6 +636,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -664,6 +686,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -712,6 +736,8 @@ INSERT INTO
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
)
@@ -724,22 +750,26 @@ VALUES (
@name,
@description,
@best_practice,
+ @implemented,
+ @not_implemented_justification,
@created_at,
@updated_at
);
`
args := pgx.StrictNamedArgs{
- "tenant_id": scope.GetTenantID(),
- "control_id": c.ID,
- "organization_id": c.OrganizationID,
- "framework_id": c.FrameworkID,
- "section_title": c.SectionTitle,
- "name": c.Name,
- "description": c.Description,
- "best_practice": c.BestPractice,
- "created_at": c.CreatedAt,
- "updated_at": c.UpdatedAt,
+ "tenant_id": scope.GetTenantID(),
+ "control_id": c.ID,
+ "organization_id": c.OrganizationID,
+ "framework_id": c.FrameworkID,
+ "section_title": c.SectionTitle,
+ "name": c.Name,
+ "description": c.Description,
+ "best_practice": c.BestPractice,
+ "implemented": c.Implemented,
+ "not_implemented_justification": c.NotImplementedJustification,
+ "created_at": c.CreatedAt,
+ "updated_at": c.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
@@ -789,6 +819,8 @@ UPDATE controls SET
description = @description,
section_title = @section_title,
best_practice = @best_practice,
+ implemented = @implemented,
+ not_implemented_justification = @not_implemented_justification,
updated_at = @updated_at
WHERE %s
AND id = @control_id
@@ -796,12 +828,14 @@ WHERE %s
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
- "control_id": c.ID,
- "name": c.Name,
- "description": c.Description,
- "section_title": c.SectionTitle,
- "best_practice": c.BestPractice,
- "updated_at": c.UpdatedAt,
+ "control_id": c.ID,
+ "name": c.Name,
+ "description": c.Description,
+ "section_title": c.SectionTitle,
+ "best_practice": c.BestPractice,
+ "implemented": c.Implemented,
+ "not_implemented_justification": c.NotImplementedJustification,
+ "updated_at": c.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())
@@ -839,6 +873,8 @@ WITH ctrl AS (
c.name,
c.description,
c.best_practice,
+ c.implemented,
+ c.not_implemented_justification,
c.created_at,
c.updated_at,
c.search_vector
@@ -857,6 +893,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
@@ -906,6 +944,8 @@ WITH ctrl AS (
c.name,
c.description,
c.best_practice,
+ c.implemented,
+ c.not_implemented_justification,
c.created_at,
c.updated_at,
c.search_vector
@@ -924,6 +964,8 @@ SELECT
name,
description,
best_practice,
+ implemented,
+ not_implemented_justification,
created_at,
updated_at
FROM
diff --git a/pkg/coredata/control_implementation_state.go b/pkg/coredata/control_implementation_state.go
new file mode 100644
index 000000000..36c4a3a7d
--- /dev/null
+++ b/pkg/coredata/control_implementation_state.go
@@ -0,0 +1,66 @@
+// Copyright (c) 2025 Probo Inc | Framework |
Control |
- Applicability |
- Justification |
- Justification for inclusion |
+ Applicability |
+ Justification for non-applicability |
+ Implemented |
+ Justification for non-implementation |
+ Justification for inclusion |
Regulatory |
- Contractual |
- Best Practice |
- Risk Assessment |
+ Regulatory |
+ Contractual |
+ Best Practice |
+ Risk Assessment |
- {{- if .Justification}}
+ {{- $appStateJ := boolToYesNo .Applicability}}
+ {{- if and (eq $appStateJ "no") .Justification}}
{{.Justification}}
{{- else}}
-
{{- end}}
|
+
+ {{- $appState := boolToYesNo .Applicability}}
+ {{- if eq $appState "no"}}
+ -
+ {{- else if .Implemented}}
+ {{- if eq (derefString .Implemented) "IMPLEMENTED"}}
+ Yes
+ {{- else}}
+ No
+ {{- end}}
+ {{- else}}
+ -
+ {{- end}}
+ |
+
+ {{- $appState2 := boolToYesNo .Applicability}}
+ {{- if eq $appState2 "no"}}
+ -
+ {{- else if and .Implemented (eq (derefString .Implemented) "NOT_IMPLEMENTED") .NotImplementedJustification}}
+ {{.NotImplementedJustification}}
+ {{- else}}
+ -
+ {{- end}}
+ |
{{boolToYesNoDash .Regulatory}} |
{{boolToYesNoDash .Contractual}} |
{{boolToYesNoDash .BestPractice}} |
@@ -383,7 +410,7 @@
-
+ Justification
+ Justification for non-applicability
+
+
+ Implemented
+
+
+
Justification for non-implementation
+
Justification for inclusion
diff --git a/pkg/probo/control_service.go b/pkg/probo/control_service.go
index 1febd288c..3085913ae 100644
--- a/pkg/probo/control_service.go
+++ b/pkg/probo/control_service.go
@@ -32,20 +32,24 @@ type (
}
CreateControlRequest struct {
- ID gid.GID
- FrameworkID gid.GID
- Name string
- Description *string
- SectionTitle string
- BestPractice bool
+ ID gid.GID
+ FrameworkID gid.GID
+ Name string
+ Description *string
+ SectionTitle string
+ BestPractice bool
+ Implemented coredata.ControlImplementationState
+ NotImplementedJustification *string
}
UpdateControlRequest struct {
- ID gid.GID
- Name *string
- Description **string
- SectionTitle *string
- BestPractice *bool
+ ID gid.GID
+ Name *string
+ Description **string
+ SectionTitle *string
+ BestPractice *bool
+ Implemented *coredata.ControlImplementationState
+ NotImplementedJustification **string
}
)
@@ -56,6 +60,17 @@ func (ccr *CreateControlRequest) Validate() error {
v.Check(ccr.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(ccr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(ccr.SectionTitle, "section_title", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
+ v.Check(ccr.NotImplementedJustification, "not_implemented_justification", validator.SafeText(ContentMaxLength))
+
+ v.Check(
+ ccr.Implemented,
+ "implemented",
+ validator.Required(),
+ validator.OneOfSlice([]string{
+ string(coredata.ControlImplementationStateImplemented),
+ string(coredata.ControlImplementationStateNotImplemented),
+ }),
+ )
return v.Error()
}
@@ -67,6 +82,15 @@ func (ucr *UpdateControlRequest) Validate() error {
v.Check(ucr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(ucr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ucr.SectionTitle, "section_title", validator.SafeTextNoNewLine(TitleMaxLength))
+ v.Check(ucr.NotImplementedJustification, "not_implemented_justification", validator.SafeText(ContentMaxLength))
+ v.Check(
+ ucr.Implemented,
+ "implemented",
+ validator.OneOfSlice([]string{
+ string(coredata.ControlImplementationStateImplemented),
+ string(coredata.ControlImplementationStateNotImplemented),
+ }),
+ )
return v.Error()
}
@@ -830,15 +854,22 @@ func (s ControlService) Create(
now := time.Now()
framework := &coredata.Framework{}
+ notImplementedJustification := req.NotImplementedJustification
+ if req.Implemented == coredata.ControlImplementationStateImplemented {
+ notImplementedJustification = nil
+ }
+
control := &coredata.Control{
- ID: gid.New(s.svc.scope.GetTenantID(), coredata.ControlEntityType),
- FrameworkID: req.FrameworkID,
- Name: req.Name,
- Description: req.Description,
- SectionTitle: req.SectionTitle,
- BestPractice: req.BestPractice,
- CreatedAt: now,
- UpdatedAt: now,
+ ID: gid.New(s.svc.scope.GetTenantID(), coredata.ControlEntityType),
+ FrameworkID: req.FrameworkID,
+ Name: req.Name,
+ Description: req.Description,
+ SectionTitle: req.SectionTitle,
+ BestPractice: req.BestPractice,
+ Implemented: req.Implemented,
+ NotImplementedJustification: notImplementedJustification,
+ CreatedAt: now,
+ UpdatedAt: now,
}
err := s.svc.pg.WithTx(
@@ -915,6 +946,17 @@ func (s ControlService) Update(
control.BestPractice = *req.BestPractice
}
+ if req.Implemented != nil {
+ control.Implemented = *req.Implemented
+ if *req.Implemented == coredata.ControlImplementationStateImplemented {
+ control.NotImplementedJustification = nil
+ }
+ }
+
+ if req.NotImplementedJustification != nil && control.Implemented == coredata.ControlImplementationStateNotImplemented {
+ control.NotImplementedJustification = *req.NotImplementedJustification
+ }
+
control.UpdatedAt = time.Now()
return control.Update(ctx, conn, s.svc.scope)
diff --git a/pkg/probo/framework_service.go b/pkg/probo/framework_service.go
index 8358536e8..50b4aefa8 100644
--- a/pkg/probo/framework_service.go
+++ b/pkg/probo/framework_service.go
@@ -68,10 +68,12 @@ type (
Dark string `json:"dark"`
} `json:"logo,omitempty"`
Controls []struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Description string `json:"description"`
- BestPractice *bool `json:"best_practice,omitempty"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ BestPractice *bool `json:"best_practice,omitempty"`
+ Implemented string `json:"implemented,omitempty"`
+ NotImplementedJustification *string `json:"not_implemented_justification,omitempty"`
} `json:"controls"`
}
}
@@ -573,16 +575,26 @@ func (s FrameworkService) Import(
if control.BestPractice != nil {
bestPractice = *control.BestPractice
}
+ implemented := coredata.ControlImplementationState(control.Implemented)
+ if !implemented.IsValid() {
+ implemented = coredata.ControlImplementationStateImplemented
+ }
+ var notImplementedJustification *string
+ if implemented == coredata.ControlImplementationStateNotImplemented {
+ notImplementedJustification = control.NotImplementedJustification
+ }
control := &coredata.Control{
- ID: controlID,
- FrameworkID: frameworkID,
- OrganizationID: organization.ID,
- SectionTitle: control.ID,
- Name: control.Name,
- Description: &description,
- BestPractice: bestPractice,
- CreatedAt: now,
- UpdatedAt: now,
+ ID: controlID,
+ FrameworkID: frameworkID,
+ OrganizationID: organization.ID,
+ SectionTitle: control.ID,
+ Name: control.Name,
+ Description: &description,
+ BestPractice: bestPractice,
+ Implemented: implemented,
+ NotImplementedJustification: notImplementedJustification,
+ CreatedAt: now,
+ UpdatedAt: now,
}
if err := control.Insert(ctx, tx, s.svc.scope); err != nil {
diff --git a/pkg/probo/state_of_applicability_service.go b/pkg/probo/state_of_applicability_service.go
index 39d2fac97..9146c9e9c 100644
--- a/pkg/probo/state_of_applicability_service.go
+++ b/pkg/probo/state_of_applicability_service.go
@@ -569,15 +569,23 @@ func (s StateOfApplicabilityService) ExportPDF(
applicability := stmt.Applicability
+ implemented := control.Implemented.String()
frameworkControlsMap[framework.Name] = append(
frameworkControlsMap[framework.Name],
docgen.ControlData{
- FrameworkName: framework.Name,
- SectionTitle: control.SectionTitle,
- Name: control.Name,
- Applicability: &applicability,
- Justification: stmt.Justification,
- BestPractice: bestPractice,
+ FrameworkName: framework.Name,
+ SectionTitle: control.SectionTitle,
+ Name: control.Name,
+ Applicability: &applicability,
+ Justification: stmt.Justification,
+ BestPractice: bestPractice,
+ Implemented: &implemented,
+ NotImplementedJustification: func() *string {
+ if control.Implemented == coredata.ControlImplementationStateImplemented {
+ return nil
+ }
+ return control.NotImplementedJustification
+ }(),
Regulatory: regulatory,
Contractual: contractual,
RiskAssessment: riskAssessment,
diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql
index 006b1c51d..858797c10 100644
--- a/pkg/server/api/console/v1/schema.graphql
+++ b/pkg/server/api/console/v1/schema.graphql
@@ -127,6 +127,20 @@ enum TrustCenterVisibility
)
}
+enum ControlImplementationState
+ @goModel(
+ model: "go.probo.inc/probo/pkg/coredata.ControlImplementationState"
+ ) {
+ IMPLEMENTED
+ @goEnum(
+ value: "go.probo.inc/probo/pkg/coredata.ControlImplementationStateImplemented"
+ )
+ NOT_IMPLEMENTED
+ @goEnum(
+ value: "go.probo.inc/probo/pkg/coredata.ControlImplementationStateNotImplemented"
+ )
+}
+
enum TrustCenterDocumentAccessStatus
@goModel(
model: "go.probo.inc/probo/pkg/coredata.TrustCenterDocumentAccessStatus"
@@ -2221,6 +2235,8 @@ type Control implements Node {
name: String!
description: String
bestPractice: Boolean!
+ implemented: ControlImplementationState!
+ notImplementedJustification: String
regulatory: Boolean! @goField(forceResolver: true)
contractual: Boolean! @goField(forceResolver: true)
riskAssessment: Boolean! @goField(forceResolver: true)
@@ -4468,6 +4484,8 @@ input CreateControlInput {
name: String!
description: String
bestPractice: Boolean!
+ implemented: ControlImplementationState!
+ notImplementedJustification: String
}
input UpdateControlInput {
@@ -4476,6 +4494,8 @@ input UpdateControlInput {
name: String
description: String @goField(omittable: true)
bestPractice: Boolean
+ implemented: ControlImplementationState
+ notImplementedJustification: String @goField(omittable: true)
}
input DeleteControlInput {
diff --git a/pkg/server/api/console/v1/types/control.go b/pkg/server/api/console/v1/types/control.go
index b36c89b8d..6e8235884 100644
--- a/pkg/server/api/console/v1/types/control.go
+++ b/pkg/server/api/console/v1/types/control.go
@@ -57,11 +57,13 @@ func NewControl(control *coredata.Control) *Control {
Framework: &Framework{
ID: control.FrameworkID,
},
- SectionTitle: control.SectionTitle,
- Name: control.Name,
- Description: control.Description,
- BestPractice: control.BestPractice,
- CreatedAt: control.CreatedAt,
- UpdatedAt: control.UpdatedAt,
+ SectionTitle: control.SectionTitle,
+ Name: control.Name,
+ Description: control.Description,
+ BestPractice: control.BestPractice,
+ Implemented: control.Implemented,
+ NotImplementedJustification: control.NotImplementedJustification,
+ CreatedAt: control.CreatedAt,
+ UpdatedAt: control.UpdatedAt,
}
}
diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go
index 9e8e985ca..a89e78466 100644
--- a/pkg/server/api/console/v1/v1_resolver.go
+++ b/pkg/server/api/console/v1/v1_resolver.go
@@ -2945,11 +2945,13 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create
control, err := prb.Controls.Create(
ctx,
probo.CreateControlRequest{
- FrameworkID: input.FrameworkID,
- Name: input.Name,
- Description: input.Description,
- SectionTitle: input.SectionTitle,
- BestPractice: input.BestPractice,
+ FrameworkID: input.FrameworkID,
+ Name: input.Name,
+ Description: input.Description,
+ SectionTitle: input.SectionTitle,
+ BestPractice: input.BestPractice,
+ Implemented: input.Implemented,
+ NotImplementedJustification: input.NotImplementedJustification,
},
)
if err != nil {
@@ -2977,11 +2979,13 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
control, err := prb.Controls.Update(
ctx,
probo.UpdateControlRequest{
- ID: input.ID,
- Name: input.Name,
- Description: gqlutils.UnwrapOmittable(input.Description),
- SectionTitle: input.SectionTitle,
- BestPractice: input.BestPractice,
+ ID: input.ID,
+ Name: input.Name,
+ Description: gqlutils.UnwrapOmittable(input.Description),
+ SectionTitle: input.SectionTitle,
+ BestPractice: input.BestPractice,
+ Implemented: input.Implemented,
+ NotImplementedJustification: gqlutils.UnwrapOmittable(input.NotImplementedJustification),
},
)
diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go
index 5881f0622..650763719 100644
--- a/pkg/server/api/mcp/v1/schema.resolvers.go
+++ b/pkg/server/api/mcp/v1/schema.resolvers.go
@@ -1550,10 +1550,13 @@ func (r *Resolver) AddControlTool(ctx context.Context, req *mcp.CallToolRequest,
control, err := svc.Controls.Create(
ctx,
probo.CreateControlRequest{
- FrameworkID: input.FrameworkID,
- Name: input.Name,
- Description: input.Description,
- SectionTitle: input.SectionTitle,
+ FrameworkID: input.FrameworkID,
+ Name: input.Name,
+ Description: input.Description,
+ SectionTitle: input.SectionTitle,
+ BestPractice: input.BestPractice,
+ Implemented: coredata.ControlImplementationState(input.Implemented),
+ NotImplementedJustification: input.NotImplementedJustification,
},
)
if err != nil {
@@ -1570,13 +1573,22 @@ func (r *Resolver) UpdateControlTool(ctx context.Context, req *mcp.CallToolReque
svc := r.ProboService(ctx, input.ID)
+ var implemented *coredata.ControlImplementationState
+ if input.Implemented != nil {
+ v := coredata.ControlImplementationState(*input.Implemented)
+ implemented = &v
+ }
+
control, err := svc.Controls.Update(
ctx,
probo.UpdateControlRequest{
- ID: input.ID,
- Name: input.Name,
- Description: UnwrapOmittable(input.Description),
- SectionTitle: input.SectionTitle,
+ ID: input.ID,
+ Name: input.Name,
+ Description: UnwrapOmittable(input.Description),
+ SectionTitle: input.SectionTitle,
+ BestPractice: input.BestPractice,
+ Implemented: implemented,
+ NotImplementedJustification: UnwrapOmittable(input.NotImplementedJustification),
},
)
if err != nil {
diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml
index 197d225fc..e8157c470 100644
--- a/pkg/server/api/mcp/v1/specification.yaml
+++ b/pkg/server/api/mcp/v1/specification.yaml
@@ -4219,6 +4219,8 @@ components:
- framework_id
- section_title
- name
+ - best_practice
+ - implemented
- created_at
- updated_at
properties:
@@ -4242,6 +4244,19 @@ components:
- string
- "null"
description: Control description
+ best_practice:
+ type: boolean
+ description: Whether control is a best practice
+ implemented:
+ type: string
+ enum: [IMPLEMENTED, NOT_IMPLEMENTED]
+ description: Control implementation state
+ go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ControlImplementationState
+ not_implemented_justification:
+ type:
+ - string
+ - "null"
+ description: Justification for non-implementation
created_at:
type: string
format: date-time
@@ -4315,6 +4330,8 @@ components:
- framework_id
- section_title
- name
+ - best_practice
+ - implemented
properties:
organization_id:
$ref: "#/components/schemas/GID"
@@ -4331,6 +4348,19 @@ components:
description:
type: string
description: Control description
+ best_practice:
+ type: boolean
+ description: Whether control is a best practice
+ implemented:
+ type: string
+ enum: [IMPLEMENTED, NOT_IMPLEMENTED]
+ description: Control implementation state
+ go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ControlImplementationState
+ not_implemented_justification:
+ type:
+ - string
+ - "null"
+ description: Justification for non-implementation
AddControlOutput:
type: object
@@ -4358,6 +4388,18 @@ components:
type: ["string", "null"]
description: Control description
go.probo.inc/mcpgen/omittable: true
+ best_practice:
+ type: boolean
+ description: Whether control is a best practice
+ implemented:
+ type: string
+ enum: [IMPLEMENTED, NOT_IMPLEMENTED]
+ description: Control implementation state
+ go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ControlImplementationState
+ not_implemented_justification:
+ type: ["string", "null"]
+ description: Justification for non-implementation
+ go.probo.inc/mcpgen/omittable: true
UpdateControlOutput:
type: object
diff --git a/pkg/server/api/mcp/v1/types/control.go b/pkg/server/api/mcp/v1/types/control.go
index 104c83880..83f519eff 100644
--- a/pkg/server/api/mcp/v1/types/control.go
+++ b/pkg/server/api/mcp/v1/types/control.go
@@ -20,14 +20,17 @@ import (
func NewControl(c *coredata.Control) *Control {
return &Control{
- ID: c.ID,
- OrganizationID: c.OrganizationID,
- SectionTitle: c.SectionTitle,
- FrameworkID: c.FrameworkID,
- Name: c.Name,
- Description: c.Description,
- CreatedAt: c.CreatedAt,
- UpdatedAt: c.UpdatedAt,
+ ID: c.ID,
+ OrganizationID: c.OrganizationID,
+ SectionTitle: c.SectionTitle,
+ FrameworkID: c.FrameworkID,
+ Name: c.Name,
+ Description: c.Description,
+ BestPractice: c.BestPractice,
+ Implemented: ControlImplemented(c.Implemented),
+ NotImplementedJustification: c.NotImplementedJustification,
+ CreatedAt: c.CreatedAt,
+ UpdatedAt: c.UpdatedAt,
}
}
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|