Add CMMI maturity level to compliance controls

Adds an optional CMMI 0-5 maturity level field to Control to support
ISO 27001 clause 9.1 effectiveness measurement and HITRUST CSF maturity
requirements. The field is nullable, framework-agnostic, and exposed
across all four API surfaces (GraphQL, MCP, CLI, n8n) plus the
generated SoA document.

Signed-off-by: Alejandro Juan <alejandrojuan@alejandrojuan.com>
This commit is contained in:
Alejandro Juan
2026-04-20 13:26:19 +02:00
committed by Sacha Al Himdani
parent 98487953b9
commit da91afc2a7
31 changed files with 919 additions and 25 deletions

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import {
controlMaturityLevels,
getControlMaturityLevelLabel,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { Option } from "@probo/ui";
export const MATURITY_LEVEL_UNSET = "__unset__" as const;
export function ControlMaturityLevelOptions() {
const { __ } = useTranslate();
return (
<>
<Option value={MATURITY_LEVEL_UNSET}>{__("Not set")}</Option>
{controlMaturityLevels.map(level => (
<Option key={level} value={level}>
{getControlMaturityLevelLabel(__, level)}
</Option>
))}
</>
);
}

View File

@@ -120,6 +120,7 @@ export const frameworkControlNodeQuery = graphql`
bestPractice
implemented
notImplementedJustification
maturityLevel
canUpdate: permission(action: "core:control:update")
canDelete: permission(action: "core:control:delete")
canCreateMeasureMapping: permission(

View File

@@ -12,7 +12,11 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { formatError, type GraphQLError } from "@probo/helpers";
import {
formatError,
getControlMaturityLevelLabel,
type GraphQLError,
} from "@probo/helpers";
import { promisifyMutation } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
@@ -390,6 +394,14 @@ export default function FrameworkControlPage({ queryRef }: Props) {
<div className="text-sm mt-0.5 whitespace-pre-wrap">{control.notImplementedJustification}</div>
</div>
)}
<div className="flex items-center gap-2">
<span className="text-sm text-txt-secondary">{__("Maturity level")}</span>
<Badge variant="neutral" size="sm">
{control.maturityLevel
? getControlMaturityLevelLabel(__, control.maturityLevel)
: __("Not set")}
</Badge>
</div>
</div>
</Card>
<div className="mb-4">

View File

@@ -21,6 +21,7 @@ import {
DialogContent,
DialogFooter,
Input,
Select,
Textarea,
useDialogRef,
} from "@probo/ui";
@@ -31,6 +32,10 @@ import { graphql } from "relay-runtime";
import { z } from "zod";
import type { FrameworkControlDialogFragment$key } from "#/__generated__/core/FrameworkControlDialogFragment.graphql";
import {
ControlMaturityLevelOptions,
MATURITY_LEVEL_UNSET,
} from "#/components/form/ControlMaturityLevelOptions";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useMutationWithToasts } from "#/hooks/useMutationWithToasts";
@@ -50,6 +55,7 @@ const controlFragment = graphql`
bestPractice
implemented
notImplementedJustification
maturityLevel
}
`;
@@ -85,6 +91,15 @@ const schema = z.object({
bestPractice: z.boolean(),
implemented: z.enum(["IMPLEMENTED", "NOT_IMPLEMENTED"]),
notImplementedJustification: z.string().optional().nullable(),
maturityLevel: z.enum([
MATURITY_LEVEL_UNSET,
"NONE",
"INITIAL",
"MANAGED",
"DEFINED",
"QUANTITATIVELY_MANAGED",
"OPTIMIZING",
]),
});
export function FrameworkControlDialog(props: Props) {
@@ -103,7 +118,7 @@ export function FrameworkControlDialog(props: Props) {
},
);
const defaultValues = useMemo(
const defaultValues = useMemo<z.infer<typeof schema>>(
() => ({
name: frameworkControl?.name ?? "",
description: frameworkControl?.description ?? "",
@@ -111,6 +126,7 @@ export function FrameworkControlDialog(props: Props) {
bestPractice: frameworkControl?.bestPractice ?? true,
implemented: frameworkControl?.implemented ?? "IMPLEMENTED",
notImplementedJustification: frameworkControl?.notImplementedJustification ?? "",
maturityLevel: frameworkControl?.maturityLevel ?? MATURITY_LEVEL_UNSET,
}),
[frameworkControl],
);
@@ -126,8 +142,13 @@ export function FrameworkControlDialog(props: Props) {
const bestPracticeValue = watch("bestPractice");
const implementedValue = watch("implemented");
const maturityLevelValue = watch("maturityLevel");
const onSubmit = async (data: z.infer<typeof schema>) => {
const maturityLevel = data.maturityLevel === MATURITY_LEVEL_UNSET
? null
: data.maturityLevel;
if (frameworkControl) {
await mutate({
variables: {
@@ -139,6 +160,7 @@ export function FrameworkControlDialog(props: Props) {
bestPractice: data.bestPractice,
implemented: data.implemented,
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
maturityLevel,
},
},
});
@@ -153,6 +175,7 @@ export function FrameworkControlDialog(props: Props) {
bestPractice: data.bestPractice ?? true,
implemented: data.implemented ?? "IMPLEMENTED",
notImplementedJustification: data.implemented === "IMPLEMENTED" ? null : (data.notImplementedJustification || null),
maturityLevel,
},
connections: [props.connectionId!],
},
@@ -226,6 +249,17 @@ export function FrameworkControlDialog(props: Props) {
{...register("notImplementedJustification")}
/>
)}
<div className="flex items-center gap-2">
<span className="text-sm">{__("Maturity level")}</span>
<Select
id="maturityLevel"
value={maturityLevelValue}
onValueChange={value =>
setValue("maturityLevel", value as typeof maturityLevelValue)}
>
<ControlMaturityLevelOptions />
</Select>
</div>
</div>
</DialogContent>
<DialogFooter>

View File

@@ -12,6 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { getControlMaturityLevelLabel } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
ActionDropdown,
@@ -87,6 +88,7 @@ export const controlsFragment = graphql`
bestPractice
implemented
notImplementedJustification
maturityLevel
regulatory
contractual
riskAssessment
@@ -151,6 +153,7 @@ export default function StatementOfApplicabilityControlsTab({
bestPractice: edge.node.control.bestPractice,
implemented: edge.node.control.implemented,
notImplementedJustification: edge.node.control.notImplementedJustification,
maturityLevel: edge.node.control.maturityLevel,
regulatory: edge.node.control.regulatory,
contractual: edge.node.control.contractual,
riskAssessment: edge.node.control.riskAssessment,
@@ -220,14 +223,15 @@ export default function StatementOfApplicabilityControlsTab({
<Table className="table-fixed w-full">
<Thead>
<Tr>
<Th className="w-[10%]">{__("Framework")}</Th>
<Th className="w-[20%]">{__("Control")}</Th>
<Th className="w-[15%]">{__("Applicability")}</Th>
<Th className="w-[15%]">{__("Implemented")}</Th>
<Th className="w-[8%]">{__("Regulatory")}</Th>
<Th className="w-[8%]">{__("Contractual")}</Th>
<Th className="w-[8%]">{__("Best Practice")}</Th>
<Th className="w-[8%]">{__("Risk Assessment")}</Th>
<Th className="w-[9%]">{__("Framework")}</Th>
<Th className="w-[17%]">{__("Control")}</Th>
<Th className="w-[12%]">{__("Applicability")}</Th>
<Th className="w-[12%]">{__("Implemented")}</Th>
<Th className="w-[14%]">{__("Maturity")}</Th>
<Th className="w-[7%]">{__("Regulatory")}</Th>
<Th className="w-[7%]">{__("Contractual")}</Th>
<Th className="w-[7%]">{__("Best Practice")}</Th>
<Th className="w-[7%]">{__("Risk Assessment")}</Th>
{(canUpdate || canDelete) && (
<Th className="w-[4%]"></Th>
)}
@@ -237,7 +241,7 @@ export default function StatementOfApplicabilityControlsTab({
{linkedControls.length === 0 && (
<Tr>
<Td
colSpan={canUpdate || canDelete ? 9 : 8}
colSpan={canUpdate || canDelete ? 10 : 9}
className="text-center text-txt-secondary py-12"
>
{__("No controls linked")}
@@ -302,6 +306,17 @@ export default function StatementOfApplicabilityControlsTab({
</div>
)}
</Td>
<Td>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>
: (
<Badge variant="neutral" size="sm">
{control.maturityLevel
? getControlMaturityLevelLabel(__, control.maturityLevel)
: __("Not set")}
</Badge>
)}
</Td>
<Td>
{control.applicability === false
? <span className="text-txt-tertiary">-</span>