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:
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,14 +186,33 @@ export function FrameworkControlDialog(props: Props) {
|
|||||||
placeholder={__("Add description")}
|
placeholder={__("Add description")}
|
||||||
{...register("description")}
|
{...register("description")}
|
||||||
/>
|
/>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<div className="border border-border-low rounded-xl p-3 space-y-3 mt-4">
|
||||||
<Checkbox
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
checked={bestPracticeValue}
|
<Checkbox
|
||||||
onChange={checked =>
|
checked={bestPracticeValue}
|
||||||
setValue("bestPractice", checked)}
|
onChange={checked =>
|
||||||
/>
|
setValue("bestPractice", checked)}
|
||||||
<span className="text-sm">{__("Best Practice")}</span>
|
/>
|
||||||
</label>
|
<span className="text-sm">{__("Best Practice")}</span>
|
||||||
|
</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}>
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ const createApplicabilityStatementMutation = graphql`
|
|||||||
sectionTitle
|
sectionTitle
|
||||||
name
|
name
|
||||||
bestPractice
|
bestPractice
|
||||||
|
implemented
|
||||||
|
notImplementedJustification
|
||||||
regulatory
|
regulatory
|
||||||
contractual
|
contractual
|
||||||
riskAssessment
|
riskAssessment
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{control.justification && (
|
||||||
</Td>
|
<p className="text-xs text-txt-secondary break-words">
|
||||||
<Td>
|
{control.justification}
|
||||||
<div className="text-sm text-txt-secondary line-clamp-2">
|
</p>
|
||||||
{control.justification || (
|
|
||||||
<span className="text-txt-tertiary italic">
|
|
||||||
-
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</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>
|
: (
|
||||||
: (
|
<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 && (
|
||||||
</div>
|
<p className="text-xs text-txt-secondary break-words">
|
||||||
|
{control.notImplementedJustification}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</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 variant="success" size="sm">{__("Yes")}</Badge>
|
||||||
<Badge
|
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||||
variant={control.contractual ? "success" : "danger"}
|
|
||||||
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 variant="success" size="sm">{__("Yes")}</Badge>
|
||||||
<Badge
|
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||||
variant={control.bestPractice ? "success" : "danger"}
|
|
||||||
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 variant="success" size="sm">{__("Yes")}</Badge>
|
||||||
<Badge
|
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||||
variant={control.riskAssessment ? "success" : "danger"}
|
</Td>
|
||||||
size="sm"
|
<Td>
|
||||||
>
|
{control.applicability === false
|
||||||
{control.riskAssessment ? __("Yes") : __("No")}
|
? <span className="text-txt-tertiary">-</span>
|
||||||
</Badge>
|
: control.riskAssessment
|
||||||
)}
|
? <Badge variant="success" size="sm">{__("Yes")}</Badge>
|
||||||
</div>
|
: <Badge variant="danger" size="sm">{__("No")}</Badge>}
|
||||||
</Td>
|
</Td>
|
||||||
{(canUpdate || canDelete) && (
|
{(canUpdate || canDelete) && (
|
||||||
<Td noLink className="text-end">
|
<Td noLink className="text-end">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ mutation($input: CreateControlInput!) {
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
bestPractice
|
bestPractice
|
||||||
|
implemented
|
||||||
|
notImplementedJustification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,11 +45,13 @@ type createResponse struct {
|
|||||||
CreateControl struct {
|
CreateControl struct {
|
||||||
ControlEdge struct {
|
ControlEdge struct {
|
||||||
Node struct {
|
Node struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
SectionTitle string `json:"sectionTitle"`
|
SectionTitle string `json:"sectionTitle"`
|
||||||
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"`
|
||||||
@@ -55,11 +59,13 @@ type createResponse struct {
|
|||||||
|
|
||||||
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||||
var (
|
var (
|
||||||
flagFramework string
|
flagFramework string
|
||||||
flagSectionTitle string
|
flagSectionTitle string
|
||||||
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")
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ mutation($input: UpdateControlInput!) {
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
bestPractice
|
bestPractice
|
||||||
|
implemented
|
||||||
|
notImplementedJustification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,21 +42,25 @@ mutation($input: UpdateControlInput!) {
|
|||||||
type updateResponse struct {
|
type updateResponse struct {
|
||||||
UpdateControl struct {
|
UpdateControl struct {
|
||||||
Control struct {
|
Control struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
SectionTitle string `json:"sectionTitle"`
|
SectionTitle string `json:"sectionTitle"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||||
var (
|
var (
|
||||||
flagSectionTitle string
|
flagSectionTitle string
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,15 +30,17 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
Control struct {
|
Control struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
SectionTitle string `db:"section_title"`
|
SectionTitle string `db:"section_title"`
|
||||||
FrameworkID gid.GID `db:"framework_id"`
|
FrameworkID gid.GID `db:"framework_id"`
|
||||||
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"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
Implemented ControlImplementationState `db:"implemented"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
NotImplementedJustification *string `db:"not_implemented_justification"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Controls []*Control
|
Controls []*Control
|
||||||
@@ -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,22 +750,26 @@ VALUES (
|
|||||||
@name,
|
@name,
|
||||||
@description,
|
@description,
|
||||||
@best_practice,
|
@best_practice,
|
||||||
|
@implemented,
|
||||||
|
@not_implemented_justification,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
);
|
);
|
||||||
`
|
`
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"control_id": c.ID,
|
"control_id": c.ID,
|
||||||
"organization_id": c.OrganizationID,
|
"organization_id": c.OrganizationID,
|
||||||
"framework_id": c.FrameworkID,
|
"framework_id": c.FrameworkID,
|
||||||
"section_title": c.SectionTitle,
|
"section_title": c.SectionTitle,
|
||||||
"name": c.Name,
|
"name": c.Name,
|
||||||
"description": c.Description,
|
"description": c.Description,
|
||||||
"best_practice": c.BestPractice,
|
"best_practice": c.BestPractice,
|
||||||
"created_at": c.CreatedAt,
|
"implemented": c.Implemented,
|
||||||
"updated_at": c.UpdatedAt,
|
"not_implemented_justification": c.NotImplementedJustification,
|
||||||
|
"created_at": c.CreatedAt,
|
||||||
|
"updated_at": c.UpdatedAt,
|
||||||
}
|
}
|
||||||
_, err := conn.Exec(ctx, q, args)
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -796,12 +828,14 @@ WHERE %s
|
|||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"control_id": c.ID,
|
"control_id": c.ID,
|
||||||
"name": c.Name,
|
"name": c.Name,
|
||||||
"description": c.Description,
|
"description": c.Description,
|
||||||
"section_title": c.SectionTitle,
|
"section_title": c.SectionTitle,
|
||||||
"best_practice": c.BestPractice,
|
"best_practice": c.BestPractice,
|
||||||
"updated_at": c.UpdatedAt,
|
"implemented": c.Implemented,
|
||||||
|
"not_implemented_justification": c.NotImplementedJustification,
|
||||||
|
"updated_at": c.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
@@ -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
|
||||||
|
|||||||
66
pkg/coredata/control_implementation_state.go
Normal file
66
pkg/coredata/control_implementation_state.go
Normal 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
|
||||||
|
}
|
||||||
5
pkg/coredata/migrations/20260316T120000Z.sql
Normal file
5
pkg/coredata/migrations/20260316T120000Z.sql
Normal 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;
|
||||||
@@ -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 "-"
|
||||||
@@ -312,15 +318,17 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
ControlData struct {
|
ControlData struct {
|
||||||
FrameworkName string
|
FrameworkName string
|
||||||
SectionTitle string
|
SectionTitle string
|
||||||
Name string
|
Name string
|
||||||
Applicability *bool
|
Applicability *bool
|
||||||
Justification *string
|
Justification *string
|
||||||
BestPractice *bool
|
BestPractice *bool
|
||||||
Regulatory *bool
|
Implemented *string
|
||||||
Contractual *bool
|
NotImplementedJustification *string
|
||||||
RiskAssessment *bool
|
Regulatory *bool
|
||||||
|
Contractual *bool
|
||||||
|
RiskAssessment *bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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;">
|
||||||
|
|||||||
@@ -32,20 +32,24 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
CreateControlRequest struct {
|
CreateControlRequest struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
FrameworkID gid.GID
|
FrameworkID gid.GID
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
SectionTitle string
|
SectionTitle string
|
||||||
BestPractice bool
|
BestPractice bool
|
||||||
|
Implemented coredata.ControlImplementationState
|
||||||
|
NotImplementedJustification *string
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateControlRequest struct {
|
UpdateControlRequest struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
Name *string
|
Name *string
|
||||||
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,15 +854,22 @@ 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,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
SectionTitle: req.SectionTitle,
|
SectionTitle: req.SectionTitle,
|
||||||
BestPractice: req.BestPractice,
|
BestPractice: req.BestPractice,
|
||||||
CreatedAt: now,
|
Implemented: req.Implemented,
|
||||||
UpdatedAt: now,
|
NotImplementedJustification: notImplementedJustification,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.svc.pg.WithTx(
|
err := s.svc.pg.WithTx(
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -68,10 +68,12 @@ type (
|
|||||||
Dark string `json:"dark"`
|
Dark string `json:"dark"`
|
||||||
} `json:"logo,omitempty"`
|
} `json:"logo,omitempty"`
|
||||||
Controls []struct {
|
Controls []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
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,16 +575,26 @@ 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,
|
||||||
OrganizationID: organization.ID,
|
OrganizationID: organization.ID,
|
||||||
SectionTitle: control.ID,
|
SectionTitle: control.ID,
|
||||||
Name: control.Name,
|
Name: control.Name,
|
||||||
Description: &description,
|
Description: &description,
|
||||||
BestPractice: bestPractice,
|
BestPractice: bestPractice,
|
||||||
CreatedAt: now,
|
Implemented: implemented,
|
||||||
UpdatedAt: now,
|
NotImplementedJustification: notImplementedJustification,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := control.Insert(ctx, tx, s.svc.scope); err != nil {
|
if err := control.Insert(ctx, tx, s.svc.scope); err != nil {
|
||||||
|
|||||||
@@ -569,15 +569,23 @@ 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{
|
||||||
FrameworkName: framework.Name,
|
FrameworkName: framework.Name,
|
||||||
SectionTitle: control.SectionTitle,
|
SectionTitle: control.SectionTitle,
|
||||||
Name: control.Name,
|
Name: control.Name,
|
||||||
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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ func NewControl(control *coredata.Control) *Control {
|
|||||||
Framework: &Framework{
|
Framework: &Framework{
|
||||||
ID: control.FrameworkID,
|
ID: control.FrameworkID,
|
||||||
},
|
},
|
||||||
SectionTitle: control.SectionTitle,
|
SectionTitle: control.SectionTitle,
|
||||||
Name: control.Name,
|
Name: control.Name,
|
||||||
Description: control.Description,
|
Description: control.Description,
|
||||||
BestPractice: control.BestPractice,
|
BestPractice: control.BestPractice,
|
||||||
CreatedAt: control.CreatedAt,
|
Implemented: control.Implemented,
|
||||||
UpdatedAt: control.UpdatedAt,
|
NotImplementedJustification: control.NotImplementedJustification,
|
||||||
|
CreatedAt: control.CreatedAt,
|
||||||
|
UpdatedAt: control.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2945,11 +2945,13 @@ func (r *mutationResolver) CreateControl(ctx context.Context, input types.Create
|
|||||||
control, err := prb.Controls.Create(
|
control, err := prb.Controls.Create(
|
||||||
ctx,
|
ctx,
|
||||||
probo.CreateControlRequest{
|
probo.CreateControlRequest{
|
||||||
FrameworkID: input.FrameworkID,
|
FrameworkID: input.FrameworkID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
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 {
|
||||||
@@ -2977,11 +2979,13 @@ func (r *mutationResolver) UpdateControl(ctx context.Context, input types.Update
|
|||||||
control, err := prb.Controls.Update(
|
control, err := prb.Controls.Update(
|
||||||
ctx,
|
ctx,
|
||||||
probo.UpdateControlRequest{
|
probo.UpdateControlRequest{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
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),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1550,10 +1550,13 @@ func (r *Resolver) AddControlTool(ctx context.Context, req *mcp.CallToolRequest,
|
|||||||
control, err := svc.Controls.Create(
|
control, err := svc.Controls.Create(
|
||||||
ctx,
|
ctx,
|
||||||
probo.CreateControlRequest{
|
probo.CreateControlRequest{
|
||||||
FrameworkID: input.FrameworkID,
|
FrameworkID: input.FrameworkID,
|
||||||
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,13 +1573,22 @@ 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{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -20,14 +20,17 @@ import (
|
|||||||
|
|
||||||
func NewControl(c *coredata.Control) *Control {
|
func NewControl(c *coredata.Control) *Control {
|
||||||
return &Control{
|
return &Control{
|
||||||
ID: c.ID,
|
ID: c.ID,
|
||||||
OrganizationID: c.OrganizationID,
|
OrganizationID: c.OrganizationID,
|
||||||
SectionTitle: c.SectionTitle,
|
SectionTitle: c.SectionTitle,
|
||||||
FrameworkID: c.FrameworkID,
|
FrameworkID: c.FrameworkID,
|
||||||
Name: c.Name,
|
Name: c.Name,
|
||||||
Description: c.Description,
|
Description: c.Description,
|
||||||
CreatedAt: c.CreatedAt,
|
BestPractice: c.BestPractice,
|
||||||
UpdatedAt: c.UpdatedAt,
|
Implemented: ControlImplemented(c.Implemented),
|
||||||
|
NotImplementedJustification: c.NotImplementedJustification,
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
UpdatedAt: c.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user