Add implemented state and justification to controls

Introduce `implemented` enum (IMPLEMENTED/NOT_IMPLEMENTED) and
`not_implemented_justification` (nullable text) fields on the Control
entity across all API surfaces (GraphQL, MCP, CLI), database, frontend,
and SOA export.

The database stores implementation state as a PostgreSQL enum
`control_implementation_state`. Controls default to IMPLEMENTED during
migration. The SOA list and PDF export show implementation status
alongside applicability, with "-" for non-applicable controls.
Justification columns are renamed for clarity: "Justification for
non-applicability" and "Justification for non-implementation".

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-03-16 19:24:34 +01:00
parent d8670d2412
commit cf1dadc0b5
25 changed files with 663 additions and 233 deletions

View File

@@ -103,6 +103,9 @@ export const frameworkControlNodeQuery = graphql`
name name
sectionTitle sectionTitle
description description
bestPractice
implemented
notImplementedJustification
canUpdate: permission(action: "core:control:update") canUpdate: permission(action: "core:control:update")
canDelete: permission(action: "core:control:delete") canDelete: permission(action: "core:control:delete")
canCreateMeasureMapping: permission( canCreateMeasureMapping: permission(

View File

@@ -3,7 +3,9 @@ import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n"; import { useTranslate } from "@probo/i18n";
import { import {
ActionDropdown, ActionDropdown,
Badge,
Button, Button,
Card,
DropdownItem, DropdownItem,
IconPencil, IconPencil,
IconTrashCan, IconTrashCan,
@@ -354,6 +356,28 @@ export default function FrameworkControlPage({ queryRef }: Props) {
{control.description} {control.description}
</div> </div>
)} )}
<Card padded className="mb-6 mt-6">
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Best Practice")}</span>
<Badge variant={control.bestPractice ? "success" : "neutral"} size="sm">
{control.bestPractice ? __("Yes") : __("No")}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Implemented")}</span>
<Badge variant={control.implemented === "IMPLEMENTED" ? "success" : "warning"} size="sm">
{control.implemented === "IMPLEMENTED" ? __("Implemented") : __("Not Implemented")}
</Badge>
</div>
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
<div>
<span className="text-xs text-txt-secondary">{__("Justification for non-implementation")}</span>
<div className="text-sm mt-0.5 whitespace-pre-wrap">{control.notImplementedJustification}</div>
</div>
)}
</div>
</Card>
<div className="mb-4"> <div className="mb-4">
<LinkedMeasuresCard <LinkedMeasuresCard
variant="card" variant="card"

View File

@@ -34,6 +34,8 @@ const controlFragment = graphql`
description description
sectionTitle sectionTitle
bestPractice bestPractice
implemented
notImplementedJustification
} }
`; `;
@@ -67,6 +69,8 @@ const schema = z.object({
description: z.string().optional().nullable(), description: z.string().optional().nullable(),
sectionTitle: z.string(), sectionTitle: z.string(),
bestPractice: z.boolean(), bestPractice: z.boolean(),
implemented: z.enum(["IMPLEMENTED", "NOT_IMPLEMENTED"]),
notImplementedJustification: z.string().optional().nullable(),
}); });
export function FrameworkControlDialog(props: Props) { export function FrameworkControlDialog(props: Props) {
@@ -91,6 +95,8 @@ export function FrameworkControlDialog(props: Props) {
description: frameworkControl?.description ?? "", description: frameworkControl?.description ?? "",
sectionTitle: frameworkControl?.sectionTitle ?? "", sectionTitle: frameworkControl?.sectionTitle ?? "",
bestPractice: frameworkControl?.bestPractice ?? true, bestPractice: frameworkControl?.bestPractice ?? true,
implemented: frameworkControl?.implemented ?? "IMPLEMENTED",
notImplementedJustification: frameworkControl?.notImplementedJustification ?? "",
}), }),
[frameworkControl], [frameworkControl],
); );
@@ -105,10 +111,10 @@ export function FrameworkControlDialog(props: Props) {
}, [defaultValues, reset]); }, [defaultValues, reset]);
const bestPracticeValue = watch("bestPractice"); const bestPracticeValue = watch("bestPractice");
const implementedValue = watch("implemented");
const onSubmit = async (data: z.infer<typeof schema>) => { const onSubmit = async (data: z.infer<typeof schema>) => {
if (frameworkControl) { if (frameworkControl) {
// Update the control
await mutate({ await mutate({
variables: { variables: {
input: { input: {
@@ -117,11 +123,12 @@ export function FrameworkControlDialog(props: Props) {
description: data.description || null, description: data.description || null,
sectionTitle: data.sectionTitle, sectionTitle: data.sectionTitle,
bestPractice: data.bestPractice, bestPractice: data.bestPractice,
implemented: data.implemented,
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
}, },
}, },
}); });
} else { } else {
// Create a new control
await mutate({ await mutate({
variables: { variables: {
input: { input: {
@@ -130,6 +137,8 @@ export function FrameworkControlDialog(props: Props) {
description: data.description || null, description: data.description || null,
sectionTitle: data.sectionTitle, sectionTitle: data.sectionTitle,
bestPractice: data.bestPractice ?? true, bestPractice: data.bestPractice ?? true,
implemented: data.implemented ?? "IMPLEMENTED",
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
}, },
connections: [props.connectionId!], connections: [props.connectionId!],
}, },
@@ -167,7 +176,7 @@ export function FrameworkControlDialog(props: Props) {
id="title" id="title"
required required
variant="title" variant="title"
placeholder={__("Document title")} placeholder={__("Control name")}
{...register("name")} {...register("name")}
/> />
<Textarea <Textarea
@@ -177,6 +186,7 @@ export function FrameworkControlDialog(props: Props) {
placeholder={__("Add description")} placeholder={__("Add description")}
{...register("description")} {...register("description")}
/> />
<div className="border border-border-low rounded-xl p-3 space-y-3 mt-4">
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<Checkbox <Checkbox
checked={bestPracticeValue} checked={bestPracticeValue}
@@ -185,6 +195,24 @@ export function FrameworkControlDialog(props: Props) {
/> />
<span className="text-sm">{__("Best Practice")}</span> <span className="text-sm">{__("Best Practice")}</span>
</label> </label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={implementedValue === "IMPLEMENTED"}
onChange={checked =>
setValue("implemented", checked ? "IMPLEMENTED" : "NOT_IMPLEMENTED")}
/>
<span className="text-sm">{__("Implemented")}</span>
</label>
{implementedValue === "NOT_IMPLEMENTED" && (
<Textarea
id="notImplementedJustification"
variant="ghost"
autogrow
placeholder={__("Justification for non-implementation")}
{...register("notImplementedJustification")}
/>
)}
</div>
</DialogContent> </DialogContent>
<DialogFooter> <DialogFooter>
<Button type="submit" disabled={isMutating}> <Button type="submit" disabled={isMutating}>

View File

@@ -79,6 +79,8 @@ const createApplicabilityStatementMutation = graphql`
sectionTitle sectionTitle
name name
bestPractice bestPractice
implemented
notImplementedJustification
regulatory regulatory
contractual contractual
riskAssessment riskAssessment

View File

@@ -71,6 +71,8 @@ export const controlsFragment = graphql`
sectionTitle sectionTitle
name name
bestPractice bestPractice
implemented
notImplementedJustification
regulatory regulatory
contractual contractual
riskAssessment riskAssessment
@@ -135,6 +137,8 @@ export default function StateOfApplicabilityControlsTab({
applicability: edge.node.applicability, applicability: edge.node.applicability,
justification: edge.node.justification, justification: edge.node.justification,
bestPractice: edge.node.control.bestPractice, bestPractice: edge.node.control.bestPractice,
implemented: edge.node.control.implemented,
notImplementedJustification: edge.node.control.notImplementedJustification,
regulatory: edge.node.control.regulatory, regulatory: edge.node.control.regulatory,
contractual: edge.node.control.contractual, contractual: edge.node.control.contractual,
riskAssessment: edge.node.control.riskAssessment, riskAssessment: edge.node.control.riskAssessment,
@@ -204,29 +208,19 @@ export default function StateOfApplicabilityControlsTab({
</div> </div>
)} )}
<Table> <Table className="table-fixed w-full">
<Thead> <Thead>
<Tr> <Tr>
<Th className="w-32">{__("Framework")}</Th> <Th className="w-[10%]">{__("Framework")}</Th>
<Th>{__("Control")}</Th> <Th className="w-[20%]">{__("Control")}</Th>
<Th className="w-28 text-center"> <Th className="w-[15%]">{__("Applicability")}</Th>
{__("Applicability")} <Th className="w-[15%]">{__("Implemented")}</Th>
</Th> <Th className="w-[8%]">{__("Regulatory")}</Th>
<Th className="min-w-48">{__("Justification")}</Th> <Th className="w-[8%]">{__("Contractual")}</Th>
<Th className="w-24 text-center"> <Th className="w-[8%]">{__("Best Practice")}</Th>
{__("Regulatory")} <Th className="w-[8%]">{__("Risk Assessment")}</Th>
</Th>
<Th className="w-24 text-center">
{__("Contractual")}
</Th>
<Th className="w-32 text-center">
{__("Best Practice")}
</Th>
<Th className="w-36 text-center">
{__("Risk Assessment")}
</Th>
{(canUpdate || canDelete) && ( {(canUpdate || canDelete) && (
<Th className="w-12"></Th> <Th className="w-[4%]"></Th>
)} )}
</Tr> </Tr>
</Thead> </Thead>
@@ -250,103 +244,82 @@ export default function StateOfApplicabilityControlsTab({
{control.frameworkName} {control.frameworkName}
</Td> </Td>
<Td> <Td>
<div className="space-y-1"> <div className="space-y-0.5">
<div className="text-xs font-medium text-txt-tertiary"> <div className="text-xs text-txt-tertiary">
{control.sectionTitle} {control.sectionTitle}
</div> </div>
<div className="text-sm"> <div className="text-xs">
{control.name} {control.name}
</div> </div>
</div> </div>
</Td> </Td>
<Td> <Td>
<div className="flex justify-center"> <div className="space-y-1">
{control.applicability !== null {control.applicability !== null
? ( ? (
<Badge <Badge
variant={ variant={control.applicability ? "success" : "danger"}
control.applicability
? "success"
: "danger"
}
size="sm" size="sm"
> >
{control.applicability {control.applicability ? __("Yes") : __("No")}
? __("Yes")
: __("No")}
</Badge> </Badge>
) )
: ( : (
<span className="text-txt-tertiary"> <span className="text-txt-tertiary">-</span>
- )}
</span> {control.justification && (
<p className="text-xs text-txt-secondary break-words">
{control.justification}
</p>
)} )}
</div> </div>
</Td> </Td>
<Td> <Td>
<div className="text-sm text-txt-secondary line-clamp-2">
{control.justification || (
<span className="text-txt-tertiary italic">
-
</span>
)}
</div>
</Td>
<Td>
<div className="flex justify-center">
{control.applicability === false {control.applicability === false
? <span className="text-txt-tertiary">-</span> ? <span className="text-txt-tertiary">-</span>
: ( : (
<div className="space-y-1">
<Badge <Badge
variant={control.regulatory ? "success" : "danger"} variant={control.implemented === "IMPLEMENTED" ? "success" : "danger"}
size="sm" size="sm"
> >
{control.regulatory ? __("Yes") : __("No")} {control.implemented === "IMPLEMENTED" ? __("Yes") : __("No")}
</Badge> </Badge>
{control.implemented === "NOT_IMPLEMENTED" && control.notImplementedJustification && (
<p className="text-xs text-txt-secondary break-words">
{control.notImplementedJustification}
</p>
)} )}
</div> </div>
)}
</Td> </Td>
<Td> <Td>
<div className="flex justify-center">
{control.applicability === false {control.applicability === false
? <span className="text-txt-tertiary">-</span> ? <span className="text-txt-tertiary">-</span>
: ( : control.regulatory
<Badge ? <Badge variant="success" size="sm">{__("Yes")}</Badge>
variant={control.contractual ? "success" : "danger"} : <Badge variant="danger" size="sm">{__("No")}</Badge>}
size="sm"
>
{control.contractual ? __("Yes") : __("No")}
</Badge>
)}
</div>
</Td> </Td>
<Td> <Td>
<div className="flex justify-center">
{control.applicability === false {control.applicability === false
? <span className="text-txt-tertiary">-</span> ? <span className="text-txt-tertiary">-</span>
: ( : control.contractual
<Badge ? <Badge variant="success" size="sm">{__("Yes")}</Badge>
variant={control.bestPractice ? "success" : "danger"} : <Badge variant="danger" size="sm">{__("No")}</Badge>}
size="sm"
>
{control.bestPractice ? __("Yes") : __("No")}
</Badge>
)}
</div>
</Td> </Td>
<Td> <Td>
<div className="flex justify-center">
{control.applicability === false {control.applicability === false
? <span className="text-txt-tertiary">-</span> ? <span className="text-txt-tertiary">-</span>
: ( : control.bestPractice
<Badge ? <Badge variant="success" size="sm">{__("Yes")}</Badge>
variant={control.riskAssessment ? "success" : "danger"} : <Badge variant="danger" size="sm">{__("No")}</Badge>}
size="sm" </Td>
> <Td>
{control.riskAssessment ? __("Yes") : __("No")} {control.applicability === false
</Badge> ? <span className="text-txt-tertiary">-</span>
)} : control.riskAssessment
</div> ? <Badge variant="success" size="sm">{__("Yes")}</Badge>
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
</Td> </Td>
{(canUpdate || canDelete) && ( {(canUpdate || canDelete) && (
<Td noLink className="text-end"> <Td noLink className="text-end">

View File

@@ -64,6 +64,7 @@ func TestControl_Create(t *testing.T) {
"name": "Information Security Policies", "name": "Information Security Policies",
"description": "Policies for information security", "description": "Policies for information security",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &result) }, &result)
require.NoError(t, err) require.NoError(t, err)
@@ -269,6 +270,7 @@ func TestControl_RequiredFields(t *testing.T) {
"description": "Test", "description": "Test",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, },
wantError: true, wantError: true,
@@ -281,6 +283,7 @@ func TestControl_RequiredFields(t *testing.T) {
"description": "Test", "description": "Test",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, },
wantError: true, wantError: true,
@@ -293,6 +296,7 @@ func TestControl_RequiredFields(t *testing.T) {
"name": "Test Control", "name": "Test Control",
"description": "Test", "description": "Test",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, },
wantError: true, wantError: true,
@@ -305,6 +309,7 @@ func TestControl_RequiredFields(t *testing.T) {
"name": "Test Control", "name": "Test Control",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, },
wantError: true, wantError: true,
@@ -317,6 +322,20 @@ func TestControl_RequiredFields(t *testing.T) {
"name": "Test Control", "name": "Test Control",
"description": "Test", "description": "Test",
"sectionTitle": "Section 1", "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, wantError: true,
@@ -404,6 +423,7 @@ func TestControl_OmittableDescription(t *testing.T) {
"description": "Initial description", "description": "Initial description",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &createResult) }, &createResult)
require.NoError(t, err) require.NoError(t, err)
@@ -568,6 +588,7 @@ func TestControl_SubResolvers(t *testing.T) {
"description": "Test description", "description": "Test description",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &controlResult) }, &controlResult)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -83,6 +83,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
"description": "Test control for mapping", "description": "Test control for mapping",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &createControlResult) }, &createControlResult)
require.NoError(t, err) require.NoError(t, err)
@@ -362,6 +363,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
"description": "Test control", "description": "Test control",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &createControlResult) }, &createControlResult)
require.NoError(t, err) require.NoError(t, err)
@@ -503,6 +505,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
"description": "Test control", "description": "Test control",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &createControlResult) }, &createControlResult)
require.NoError(t, err) require.NoError(t, err)
@@ -640,6 +643,7 @@ func TestControlSnapshotMapping_CreateDelete(t *testing.T) {
"description": "Test control", "description": "Test control",
"sectionTitle": "Section 1", "sectionTitle": "Section 1",
"bestPractice": true, "bestPractice": true,
"implemented": "IMPLEMENTED",
}, },
}, &createControlResult) }, &createControlResult)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -384,7 +384,7 @@ func TestRBAC(t *testing.T) {
client: owner, client: owner,
query: createControlMutation, query: createControlMutation,
variables: func() map[string]any { 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, shouldAllow: true,
}, },
@@ -394,7 +394,7 @@ func TestRBAC(t *testing.T) {
client: admin, client: admin,
query: createControlMutation, query: createControlMutation,
variables: func() map[string]any { 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, shouldAllow: true,
}, },
@@ -404,7 +404,7 @@ func TestRBAC(t *testing.T) {
client: viewer, client: viewer,
query: createControlMutation, query: createControlMutation,
variables: func() map[string]any { 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, shouldAllow: false,
}, },

View File

@@ -252,6 +252,11 @@ func CreateControl(c *testutil.Client, frameworkID string, attrs ...Attrs) strin
"description": a.getString("description", "Test control description"), "description": a.getString("description", "Test control description"),
"sectionTitle": a.getString("sectionTitle", fmt.Sprintf("Section %s", gofakeit.LetterN(3))), "sectionTitle": a.getString("sectionTitle", fmt.Sprintf("Section %s", gofakeit.LetterN(3))),
"bestPractice": a.getBool("bestPractice", true), "bestPractice": a.getBool("bestPractice", true),
"implemented": a.getString("implemented", "IMPLEMENTED"),
}
if justification := a.getStringPtr("notImplementedJustification"); justification != nil {
input["notImplementedJustification"] = *justification
} }
var result struct { var result struct {
@@ -495,6 +500,16 @@ func (b *ControlBuilder) WithBestPractice(bestPractice bool) *ControlBuilder {
return b 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 { func (b *ControlBuilder) Create() string {
return CreateControl(b.client, b.frameworkID, b.attrs) return CreateControl(b.client, b.frameworkID, b.attrs)
} }

View File

@@ -33,6 +33,8 @@ mutation($input: CreateControlInput!) {
name name
description description
bestPractice bestPractice
implemented
notImplementedJustification
} }
} }
} }
@@ -48,6 +50,8 @@ type createResponse struct {
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
BestPractice bool `json:"bestPractice"` BestPractice bool `json:"bestPractice"`
Implemented string `json:"implemented"`
NotImplementedJustification *string `json:"notImplementedJustification"`
} `json:"node"` } `json:"node"`
} `json:"controlEdge"` } `json:"controlEdge"`
} `json:"createControl"` } `json:"createControl"`
@@ -60,6 +64,8 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
flagName string flagName string
flagDescription string flagDescription string
flagBestPractice bool flagBestPractice bool
flagNotImplemented bool
flagNotImplementedJustification string
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -86,17 +92,27 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
cfg.HTTPTimeoutDuration(), cfg.HTTPTimeoutDuration(),
) )
implemented := "IMPLEMENTED"
if flagNotImplemented {
implemented = "NOT_IMPLEMENTED"
}
input := map[string]any{ input := map[string]any{
"frameworkId": flagFramework, "frameworkId": flagFramework,
"sectionTitle": flagSectionTitle, "sectionTitle": flagSectionTitle,
"name": flagName, "name": flagName,
"bestPractice": flagBestPractice, "bestPractice": flagBestPractice,
"implemented": implemented,
} }
if flagDescription != "" { if flagDescription != "" {
input["description"] = flagDescription input["description"] = flagDescription
} }
if flagNotImplemented && flagNotImplementedJustification != "" {
input["notImplementedJustification"] = flagNotImplementedJustification
}
data, err := client.Do( data, err := client.Do(
createMutation, createMutation,
map[string]any{"input": input}, 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(&flagName, "name", "", "Control name (required)")
cmd.Flags().StringVar(&flagDescription, "description", "", "Control description") cmd.Flags().StringVar(&flagDescription, "description", "", "Control description")
cmd.Flags().BoolVar(&flagBestPractice, "best-practice", false, "Mark as best practice") 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("framework")
_ = cmd.MarkFlagRequired("section-title") _ = cmd.MarkFlagRequired("section-title")

View File

@@ -32,6 +32,8 @@ mutation($input: UpdateControlInput!) {
name name
description description
bestPractice bestPractice
implemented
notImplementedJustification
} }
} }
} }
@@ -45,6 +47,8 @@ type updateResponse struct {
Name string `json:"name"` Name string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
BestPractice bool `json:"bestPractice"` BestPractice bool `json:"bestPractice"`
Implemented string `json:"implemented"`
NotImplementedJustification *string `json:"notImplementedJustification"`
} `json:"control"` } `json:"control"`
} `json:"updateControl"` } `json:"updateControl"`
} }
@@ -55,6 +59,8 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
flagName string flagName string
flagDescription string flagDescription string
flagBestPractice bool flagBestPractice bool
flagNotImplemented bool
flagNotImplementedJustification string
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -99,6 +105,20 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
if cmd.Flags().Changed("best-practice") { if cmd.Flags().Changed("best-practice") {
input["bestPractice"] = flagBestPractice 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 { if len(input) == 1 {
return fmt.Errorf("at least one field must be specified for update") 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(&flagName, "name", "", "Control name")
cmd.Flags().StringVar(&flagDescription, "description", "", "Control description") cmd.Flags().StringVar(&flagDescription, "description", "", "Control description")
cmd.Flags().BoolVar(&flagBestPractice, "best-practice", false, "Mark as best practice") 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 return cmd
} }

View File

@@ -37,6 +37,8 @@ type (
Name string `db:"name"` Name string `db:"name"`
Description *string `db:"description"` Description *string `db:"description"`
BestPractice bool `db:"best_practice"` BestPractice bool `db:"best_practice"`
Implemented ControlImplementationState `db:"implemented"`
NotImplementedJustification *string `db:"not_implemented_justification"`
CreatedAt time.Time `db:"created_at"` CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt time.Time `db:"updated_at"`
} }
@@ -131,6 +133,8 @@ WITH ctrl AS (
c.name, c.name,
c.description, c.description,
c.best_practice, c.best_practice,
c.implemented,
c.not_implemented_justification,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
c.search_vector c.search_vector
@@ -149,6 +153,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -240,6 +246,8 @@ WITH ctrl AS (
c.name, c.name,
c.description, c.description,
c.best_practice, c.best_practice,
c.implemented,
c.not_implemented_justification,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
c.search_vector c.search_vector
@@ -258,6 +266,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -355,6 +365,8 @@ WITH ctrl AS (
c.name, c.name,
c.description, c.description,
c.best_practice, c.best_practice,
c.implemented,
c.not_implemented_justification,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
c.search_vector c.search_vector
@@ -379,6 +391,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -458,6 +472,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -552,6 +568,8 @@ WITH ctrl AS (
c.name, c.name,
c.description, c.description,
c.best_practice, c.best_practice,
c.implemented,
c.not_implemented_justification,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
c.search_vector c.search_vector
@@ -570,6 +588,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -616,6 +636,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -664,6 +686,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -712,6 +736,8 @@ INSERT INTO
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
) )
@@ -724,6 +750,8 @@ VALUES (
@name, @name,
@description, @description,
@best_practice, @best_practice,
@implemented,
@not_implemented_justification,
@created_at, @created_at,
@updated_at @updated_at
); );
@@ -738,6 +766,8 @@ VALUES (
"name": c.Name, "name": c.Name,
"description": c.Description, "description": c.Description,
"best_practice": c.BestPractice, "best_practice": c.BestPractice,
"implemented": c.Implemented,
"not_implemented_justification": c.NotImplementedJustification,
"created_at": c.CreatedAt, "created_at": c.CreatedAt,
"updated_at": c.UpdatedAt, "updated_at": c.UpdatedAt,
} }
@@ -789,6 +819,8 @@ UPDATE controls SET
description = @description, description = @description,
section_title = @section_title, section_title = @section_title,
best_practice = @best_practice, best_practice = @best_practice,
implemented = @implemented,
not_implemented_justification = @not_implemented_justification,
updated_at = @updated_at updated_at = @updated_at
WHERE %s WHERE %s
AND id = @control_id AND id = @control_id
@@ -801,6 +833,8 @@ WHERE %s
"description": c.Description, "description": c.Description,
"section_title": c.SectionTitle, "section_title": c.SectionTitle,
"best_practice": c.BestPractice, "best_practice": c.BestPractice,
"implemented": c.Implemented,
"not_implemented_justification": c.NotImplementedJustification,
"updated_at": c.UpdatedAt, "updated_at": c.UpdatedAt,
} }
@@ -839,6 +873,8 @@ WITH ctrl AS (
c.name, c.name,
c.description, c.description,
c.best_practice, c.best_practice,
c.implemented,
c.not_implemented_justification,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
c.search_vector c.search_vector
@@ -857,6 +893,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM
@@ -906,6 +944,8 @@ WITH ctrl AS (
c.name, c.name,
c.description, c.description,
c.best_practice, c.best_practice,
c.implemented,
c.not_implemented_justification,
c.created_at, c.created_at,
c.updated_at, c.updated_at,
c.search_vector c.search_vector
@@ -924,6 +964,8 @@ SELECT
name, name,
description, description,
best_practice, best_practice,
implemented,
not_implemented_justification,
created_at, created_at,
updated_at updated_at
FROM FROM

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
ControlImplementationState string
)
const (
ControlImplementationStateImplemented ControlImplementationState = "IMPLEMENTED"
ControlImplementationStateNotImplemented ControlImplementationState = "NOT_IMPLEMENTED"
)
func (s ControlImplementationState) IsValid() bool {
switch s {
case ControlImplementationStateImplemented, ControlImplementationStateNotImplemented:
return true
}
return false
}
func (s ControlImplementationState) String() string {
return string(s)
}
func (s ControlImplementationState) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
func (s *ControlImplementationState) UnmarshalText(data []byte) error {
val := ControlImplementationState(data)
if !val.IsValid() {
return fmt.Errorf("invalid ControlImplementationState value: %q", string(data))
}
*s = val
return nil
}
func (s *ControlImplementationState) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for ControlImplementationState, expected string got %T", value)
}
return s.UnmarshalText([]byte(val))
}
func (s ControlImplementationState) Value() (driver.Value, error) {
return s.String(), nil
}

View File

@@ -0,0 +1,5 @@
-- Add implemented state and not_implemented_justification columns to controls table
CREATE TYPE control_implementation_state AS ENUM ('IMPLEMENTED', 'NOT_IMPLEMENTED');
ALTER TABLE controls ADD COLUMN implemented control_implementation_state NOT NULL DEFAULT 'IMPLEMENTED';
ALTER TABLE controls ADD COLUMN not_implemented_justification text;
ALTER TABLE controls ALTER COLUMN implemented DROP DEFAULT;

View File

@@ -65,6 +65,12 @@ var (
} }
return "no" return "no"
}, },
"derefString": func(s *string) string {
if s == nil {
return ""
}
return *s
},
"boolToYesNoDash": func(b *bool) string { "boolToYesNoDash": func(b *bool) string {
if b == nil { if b == nil {
return "-" return "-"
@@ -318,6 +324,8 @@ type (
Applicability *bool Applicability *bool
Justification *string Justification *string
BestPractice *bool BestPractice *bool
Implemented *string
NotImplementedJustification *string
Regulatory *bool Regulatory *bool
Contractual *bool Contractual *bool
RiskAssessment *bool RiskAssessment *bool

View File

@@ -297,15 +297,17 @@
<tr> <tr>
<th rowspan="2" style="width: 12%;">Framework</th> <th rowspan="2" style="width: 12%;">Framework</th>
<th rowspan="2" style="width: 24%;">Control</th> <th rowspan="2" style="width: 24%;">Control</th>
<th rowspan="2" style="width: 9%;">Applicability</th> <th rowspan="2" style="width: 8%;">Applicability</th>
<th rowspan="2" style="width: 17%;">Justification</th> <th rowspan="2" style="width: 14%;">Justification for non-applicability</th>
<th colspan="4" style="width: 38%; text-align: center;">Justification for inclusion</th> <th rowspan="2" style="width: 8%;">Implemented</th>
<th rowspan="2" style="width: 10%;">Justification for non-implementation</th>
<th colspan="4" style="width: 24%; text-align: center;">Justification for inclusion</th>
</tr> </tr>
<tr> <tr>
<th style="width: 8%;">Regulatory</th> <th style="width: 6%;">Regulatory</th>
<th style="width: 8%;">Contractual</th> <th style="width: 6%;">Contractual</th>
<th style="width: 10%;">Best Practice</th> <th style="width: 6%;">Best Practice</th>
<th style="width: 12%;">Risk Assessment</th> <th style="width: 6%;">Risk Assessment</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -325,12 +327,37 @@
{{- end}} {{- end}}
</td> </td>
<td> <td>
{{- if .Justification}} {{- $appStateJ := boolToYesNo .Applicability}}
{{- if and (eq $appStateJ "no") .Justification}}
{{.Justification}} {{.Justification}}
{{- else}} {{- else}}
- -
{{- end}} {{- end}}
</td> </td>
<td>
{{- $appState := boolToYesNo .Applicability}}
{{- if eq $appState "no"}}
<span class="state-tag">-</span>
{{- else if .Implemented}}
{{- if eq (derefString .Implemented) "IMPLEMENTED"}}
<span class="state-tag state-tag-success">Yes</span>
{{- else}}
<span class="state-tag state-tag-danger">No</span>
{{- end}}
{{- else}}
<span class="state-tag">-</span>
{{- end}}
</td>
<td>
{{- $appState2 := boolToYesNo .Applicability}}
{{- if eq $appState2 "no"}}
-
{{- else if and .Implemented (eq (derefString .Implemented) "NOT_IMPLEMENTED") .NotImplementedJustification}}
{{.NotImplementedJustification}}
{{- else}}
-
{{- end}}
</td>
<td>{{boolToYesNoDash .Regulatory}}</td> <td>{{boolToYesNoDash .Regulatory}}</td>
<td>{{boolToYesNoDash .Contractual}}</td> <td>{{boolToYesNoDash .Contractual}}</td>
<td>{{boolToYesNoDash .BestPractice}}</td> <td>{{boolToYesNoDash .BestPractice}}</td>
@@ -383,7 +410,7 @@
</div> </div>
<div class="annex-section"> <div class="annex-section">
<div class="annex-subsection-title">Justification</div> <div class="annex-subsection-title">Justification for non-applicability</div>
<ul class="annex-enum-list"> <ul class="annex-enum-list">
<li class="annex-enum-item"> <li class="annex-enum-item">
<span class="annex-enum-description">Provides the rationale when a control is not applicable. This field is empty for applicable controls.</span> <span class="annex-enum-description">Provides the rationale when a control is not applicable. This field is empty for applicable controls.</span>
@@ -391,6 +418,33 @@
</ul> </ul>
</div> </div>
<div class="annex-section">
<div class="annex-subsection-title">Implemented</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-name">Yes:</span>
<span class="annex-enum-description">The control has been implemented by the organization.</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">No:</span>
<span class="annex-enum-description">The control has not been implemented (with justification provided).</span>
</li>
<li class="annex-enum-item">
<span class="annex-enum-name">-:</span>
<span class="annex-enum-description">Not applicable (control is not applicable).</span>
</li>
</ul>
</div>
<div class="annex-section">
<div class="annex-subsection-title">Justification for non-implementation</div>
<ul class="annex-enum-list">
<li class="annex-enum-item">
<span class="annex-enum-description">Provides the rationale when a control is not implemented. This field is empty for implemented controls or when the control is not applicable.</span>
</li>
</ul>
</div>
<div class="annex-section"> <div class="annex-section">
<div class="annex-subsection-title">Justification for inclusion</div> <div class="annex-subsection-title">Justification for inclusion</div>
<div class="annex-enum-description" style="margin-bottom: 12px;"> <div class="annex-enum-description" style="margin-bottom: 12px;">

View File

@@ -38,6 +38,8 @@ type (
Description *string Description *string
SectionTitle string SectionTitle string
BestPractice bool BestPractice bool
Implemented coredata.ControlImplementationState
NotImplementedJustification *string
} }
UpdateControlRequest struct { UpdateControlRequest struct {
@@ -46,6 +48,8 @@ type (
Description **string Description **string
SectionTitle *string SectionTitle *string
BestPractice *bool 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.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(ccr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength)) v.Check(ccr.Description, "description", validator.Required(), validator.SafeText(ContentMaxLength))
v.Check(ccr.SectionTitle, "section_title", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength)) 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() return v.Error()
} }
@@ -67,6 +82,15 @@ func (ucr *UpdateControlRequest) Validate() error {
v.Check(ucr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength)) v.Check(ucr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
v.Check(ucr.Description, "description", validator.SafeText(ContentMaxLength)) v.Check(ucr.Description, "description", validator.SafeText(ContentMaxLength))
v.Check(ucr.SectionTitle, "section_title", validator.SafeTextNoNewLine(TitleMaxLength)) 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() return v.Error()
} }
@@ -830,6 +854,11 @@ func (s ControlService) Create(
now := time.Now() now := time.Now()
framework := &coredata.Framework{} framework := &coredata.Framework{}
notImplementedJustification := req.NotImplementedJustification
if req.Implemented == coredata.ControlImplementationStateImplemented {
notImplementedJustification = nil
}
control := &coredata.Control{ control := &coredata.Control{
ID: gid.New(s.svc.scope.GetTenantID(), coredata.ControlEntityType), ID: gid.New(s.svc.scope.GetTenantID(), coredata.ControlEntityType),
FrameworkID: req.FrameworkID, FrameworkID: req.FrameworkID,
@@ -837,6 +866,8 @@ func (s ControlService) Create(
Description: req.Description, Description: req.Description,
SectionTitle: req.SectionTitle, SectionTitle: req.SectionTitle,
BestPractice: req.BestPractice, BestPractice: req.BestPractice,
Implemented: req.Implemented,
NotImplementedJustification: notImplementedJustification,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
@@ -915,6 +946,17 @@ func (s ControlService) Update(
control.BestPractice = *req.BestPractice 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() control.UpdatedAt = time.Now()
return control.Update(ctx, conn, s.svc.scope) return control.Update(ctx, conn, s.svc.scope)

View File

@@ -72,6 +72,8 @@ type (
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
BestPractice *bool `json:"best_practice,omitempty"` BestPractice *bool `json:"best_practice,omitempty"`
Implemented string `json:"implemented,omitempty"`
NotImplementedJustification *string `json:"not_implemented_justification,omitempty"`
} `json:"controls"` } `json:"controls"`
} }
} }
@@ -573,6 +575,14 @@ func (s FrameworkService) Import(
if control.BestPractice != nil { if control.BestPractice != nil {
bestPractice = *control.BestPractice 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{ control := &coredata.Control{
ID: controlID, ID: controlID,
FrameworkID: frameworkID, FrameworkID: frameworkID,
@@ -581,6 +591,8 @@ func (s FrameworkService) Import(
Name: control.Name, Name: control.Name,
Description: &description, Description: &description,
BestPractice: bestPractice, BestPractice: bestPractice,
Implemented: implemented,
NotImplementedJustification: notImplementedJustification,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }

View File

@@ -569,6 +569,7 @@ func (s StateOfApplicabilityService) ExportPDF(
applicability := stmt.Applicability applicability := stmt.Applicability
implemented := control.Implemented.String()
frameworkControlsMap[framework.Name] = append( frameworkControlsMap[framework.Name] = append(
frameworkControlsMap[framework.Name], frameworkControlsMap[framework.Name],
docgen.ControlData{ docgen.ControlData{
@@ -578,6 +579,13 @@ func (s StateOfApplicabilityService) ExportPDF(
Applicability: &applicability, Applicability: &applicability,
Justification: stmt.Justification, Justification: stmt.Justification,
BestPractice: bestPractice, BestPractice: bestPractice,
Implemented: &implemented,
NotImplementedJustification: func() *string {
if control.Implemented == coredata.ControlImplementationStateImplemented {
return nil
}
return control.NotImplementedJustification
}(),
Regulatory: regulatory, Regulatory: regulatory,
Contractual: contractual, Contractual: contractual,
RiskAssessment: riskAssessment, RiskAssessment: riskAssessment,

View File

@@ -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 enum TrustCenterDocumentAccessStatus
@goModel( @goModel(
model: "go.probo.inc/probo/pkg/coredata.TrustCenterDocumentAccessStatus" model: "go.probo.inc/probo/pkg/coredata.TrustCenterDocumentAccessStatus"
@@ -2221,6 +2235,8 @@ type Control implements Node {
name: String! name: String!
description: String description: String
bestPractice: Boolean! bestPractice: Boolean!
implemented: ControlImplementationState!
notImplementedJustification: String
regulatory: Boolean! @goField(forceResolver: true) regulatory: Boolean! @goField(forceResolver: true)
contractual: Boolean! @goField(forceResolver: true) contractual: Boolean! @goField(forceResolver: true)
riskAssessment: Boolean! @goField(forceResolver: true) riskAssessment: Boolean! @goField(forceResolver: true)
@@ -4468,6 +4484,8 @@ input CreateControlInput {
name: String! name: String!
description: String description: String
bestPractice: Boolean! bestPractice: Boolean!
implemented: ControlImplementationState!
notImplementedJustification: String
} }
input UpdateControlInput { input UpdateControlInput {
@@ -4476,6 +4494,8 @@ input UpdateControlInput {
name: String name: String
description: String @goField(omittable: true) description: String @goField(omittable: true)
bestPractice: Boolean bestPractice: Boolean
implemented: ControlImplementationState
notImplementedJustification: String @goField(omittable: true)
} }
input DeleteControlInput { input DeleteControlInput {

View File

@@ -61,6 +61,8 @@ func NewControl(control *coredata.Control) *Control {
Name: control.Name, Name: control.Name,
Description: control.Description, Description: control.Description,
BestPractice: control.BestPractice, BestPractice: control.BestPractice,
Implemented: control.Implemented,
NotImplementedJustification: control.NotImplementedJustification,
CreatedAt: control.CreatedAt, CreatedAt: control.CreatedAt,
UpdatedAt: control.UpdatedAt, UpdatedAt: control.UpdatedAt,
} }

View File

@@ -2950,6 +2950,8 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create
Description: input.Description, Description: input.Description,
SectionTitle: input.SectionTitle, SectionTitle: input.SectionTitle,
BestPractice: input.BestPractice, BestPractice: input.BestPractice,
Implemented: input.Implemented,
NotImplementedJustification: input.NotImplementedJustification,
}, },
) )
if err != nil { if err != nil {
@@ -2982,6 +2984,8 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
Description: gqlutils.UnwrapOmittable(input.Description), Description: gqlutils.UnwrapOmittable(input.Description),
SectionTitle: input.SectionTitle, SectionTitle: input.SectionTitle,
BestPractice: input.BestPractice, BestPractice: input.BestPractice,
Implemented: input.Implemented,
NotImplementedJustification: gqlutils.UnwrapOmittable(input.NotImplementedJustification),
}, },
) )

View File

@@ -1554,6 +1554,9 @@ func (r *Resolver) AddControlTool(ctx context.Context, req *mcp.CallToolRequest,
Name: input.Name, Name: input.Name,
Description: input.Description, Description: input.Description,
SectionTitle: input.SectionTitle, SectionTitle: input.SectionTitle,
BestPractice: input.BestPractice,
Implemented: coredata.ControlImplementationState(input.Implemented),
NotImplementedJustification: input.NotImplementedJustification,
}, },
) )
if err != nil { if err != nil {
@@ -1570,6 +1573,12 @@ func (r *Resolver) UpdateControlTool(ctx context.Context, req *mcp.CallToolReque
svc := r.ProboService(ctx, input.ID) 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( control, err := svc.Controls.Update(
ctx, ctx,
probo.UpdateControlRequest{ probo.UpdateControlRequest{
@@ -1577,6 +1586,9 @@ func (r *Resolver) UpdateControlTool(ctx context.Context, req *mcp.CallToolReque
Name: input.Name, Name: input.Name,
Description: UnwrapOmittable(input.Description), Description: UnwrapOmittable(input.Description),
SectionTitle: input.SectionTitle, SectionTitle: input.SectionTitle,
BestPractice: input.BestPractice,
Implemented: implemented,
NotImplementedJustification: UnwrapOmittable(input.NotImplementedJustification),
}, },
) )
if err != nil { if err != nil {

View File

@@ -4219,6 +4219,8 @@ components:
- framework_id - framework_id
- section_title - section_title
- name - name
- best_practice
- implemented
- created_at - created_at
- updated_at - updated_at
properties: properties:
@@ -4242,6 +4244,19 @@ components:
- string - string
- "null" - "null"
description: Control description 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: created_at:
type: string type: string
format: date-time format: date-time
@@ -4315,6 +4330,8 @@ components:
- framework_id - framework_id
- section_title - section_title
- name - name
- best_practice
- implemented
properties: properties:
organization_id: organization_id:
$ref: "#/components/schemas/GID" $ref: "#/components/schemas/GID"
@@ -4331,6 +4348,19 @@ components:
description: description:
type: string type: string
description: Control description 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: AddControlOutput:
type: object type: object
@@ -4358,6 +4388,18 @@ components:
type: ["string", "null"] type: ["string", "null"]
description: Control description description: Control description
go.probo.inc/mcpgen/omittable: true 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: UpdateControlOutput:
type: object type: object

View File

@@ -26,6 +26,9 @@ func NewControl(c *coredata.Control) *Control {
FrameworkID: c.FrameworkID, FrameworkID: c.FrameworkID,
Name: c.Name, Name: c.Name,
Description: c.Description, Description: c.Description,
BestPractice: c.BestPractice,
Implemented: ControlImplemented(c.Implemented),
NotImplementedJustification: c.NotImplementedJustification,
CreatedAt: c.CreatedAt, CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt, UpdatedAt: c.UpdatedAt,
} }