diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9b533f4..3f4ff1009 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ All notable changes to this project will be documented in this file. - Add import framework support. - Add risk object management. - Add risk template. -- Add mapping between control and risk. + - Add mapping between control and risk. ### Changed diff --git a/apps/console/src/pages/organizations/mitigations/MitigationView.tsx b/apps/console/src/pages/organizations/mitigations/MitigationView.tsx index ea1ce579c..a43fe7aa8 100644 --- a/apps/console/src/pages/organizations/mitigations/MitigationView.tsx +++ b/apps/console/src/pages/organizations/mitigations/MitigationView.tsx @@ -38,6 +38,7 @@ import { Link as LinkIcon, CheckSquare, ShieldCheck, + AlertTriangle, } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { useToast } from "@/hooks/use-toast"; @@ -88,7 +89,10 @@ import { MitigationViewAssignTaskMutation as MitigationViewAssignTaskMutationTyp import { MitigationViewUnassignTaskMutation as MitigationViewUnassignTaskMutationType } from "./__generated__/MitigationViewUnassignTaskMutation.graphql"; import { MitigationViewUpdateMitigationStateMutation as MitigationViewUpdateMitigationStateMutationType } from "./__generated__/MitigationViewUpdateMitigationStateMutation.graphql"; import { MitigationViewQuery as MitigationViewQueryType } from "./__generated__/MitigationViewQuery.graphql"; -import { MitigationViewOrganizationQuery$data } from "./__generated__/MitigationViewOrganizationQuery.graphql"; +import { + MitigationViewOrganizationQuery, + MitigationViewOrganizationQuery$data, +} from "./__generated__/MitigationViewOrganizationQuery.graphql"; import { MitigationViewFrameworksQuery, MitigationViewFrameworksQuery$data, @@ -97,6 +101,10 @@ import { MitigationViewLinkedControlsQuery, MitigationViewLinkedControlsQuery$data, } from "./__generated__/MitigationViewLinkedControlsQuery.graphql"; +import { + MitigationViewRisksQuery, + MitigationViewRisksQuery$data, +} from "./__generated__/MitigationViewRisksQuery.graphql"; // Function to format ISO8601 duration to human-readable format const formatDuration = (isoDuration: string): string => { @@ -408,6 +416,30 @@ const deleteControlMappingMutation = graphql` } `; +// Add query to fetch risks linked to a mitigation +const mitigationRisksQuery = graphql` + query MitigationViewRisksQuery($mitigationId: ID!) { + mitigation: node(id: $mitigationId) { + id + ... on Mitigation { + risks(first: 100) @connection(key: "MitigationView_risks") { + edges { + node { + id + name + description + probability + impact + createdAt + updatedAt + } + } + } + } + } + } +`; + // Define type for framework structure type FrameworkEdge = { node: { @@ -433,6 +465,29 @@ type ControlNode = { description?: string; }; +// Define Risk type +type RiskNode = { + id: string; + name: string; + description: string; + probability: number; + impact: number; + createdAt: string; + updatedAt: string; +}; + +// Define the risks data type +type MitigationRisksData = { + mitigation?: { + id: string; + risks?: { + edges: Array<{ + node: RiskNode; + }>; + }; + }; +}; + function MitigationViewContent({ queryRef, }: { @@ -508,6 +563,25 @@ function MitigationViewContent({ } }, [environment, mitigationId]); + // Add state for risks data + const [risksData, setRisksData] = useState(null); + + // Load risks data when component mounts + useEffect(() => { + if (mitigationId) { + fetchQuery(environment, mitigationRisksQuery, { mitigationId }).subscribe( + { + next: (data: unknown) => { + setRisksData(data as MitigationRisksData); + }, + error: (error: Error) => { + console.error("Error fetching risks:", error); + }, + } + ); + } + }, [environment, mitigationId]); + const formatImportance = (importance: string | undefined): string => { if (!importance) return ""; @@ -1618,6 +1692,47 @@ function MitigationViewContent({ ); }, [controlSearchQuery, getControls]); + // Helper function to get risk severity color + const getRiskSeverityColor = ( + probability: number, + impact: number + ): string => { + const severity = probability * impact; + + if (severity >= 0.75) return "bg-red-100 text-red-800"; + if (severity >= 0.5) return "bg-orange-100 text-orange-800"; + if (severity >= 0.25) return "bg-yellow-100 text-yellow-800"; + return "bg-green-100 text-green-800"; + }; + + // Helper function to get risk severity text + const getRiskSeverityText = (probability: number, impact: number): string => { + const severity = probability * impact; + + if (severity >= 0.75) return "Critical"; + if (severity >= 0.5) return "High"; + if (severity >= 0.25) return "Medium"; + return "Low"; + }; + + // Format probability as text + const formatProbability = (value: number): string => { + if (value <= 0.1) return "Very Low"; + if (value <= 0.3) return "Low"; + if (value <= 0.5) return "Medium"; + if (value <= 0.7) return "High"; + return "Very High"; + }; + + // Format impact as text + const formatImpact = (value: number): string => { + if (value <= 0.1) return "Very Low"; + if (value <= 0.3) return "Low"; + if (value <= 0.5) return "Medium"; + if (value <= 0.7) return "High"; + return "Very High"; + }; + return ( )} + + + Risks + {risksData?.mitigation?.risks?.edges && + risksData.mitigation.risks.edges.length > 0 && ( + + {risksData.mitigation.risks.edges.length} + + )} + {/* Controls Tab Content */} @@ -2490,6 +2615,88 @@ function MitigationViewContent({ + + {/* Risks Tab Content */} + +
+
+

Risks

+
+ + + + {risksData?.mitigation?.risks?.edges && + risksData.mitigation.risks.edges.length > 0 ? ( +
+ + + + + + + + + + + {risksData.mitigation.risks.edges.map(({ node }) => ( + + navigate( + `/organizations/${organizationId}/risks/${node.id}` + ) + } + style={{ cursor: "pointer" }} + > + + + + + + ))} + +
+ Risk Name + + Probability + + Impact + + Severity +
+ {node.name} + +
+ {formatProbability(node.probability)} +
+
+
+ {formatImpact(node.impact)} +
+
+
+ {getRiskSeverityText( + node.probability, + node.impact + )} +
+
+
+ ) : ( +
+ No risks linked to this mitigation yet. +
+ )} +
+
+
+
{/* Right task panel */} diff --git a/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts b/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts new file mode 100644 index 000000000..a4396efcf --- /dev/null +++ b/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts @@ -0,0 +1,286 @@ +/** + * @generated SignedSource<<7ecc796e34bf88aa378e81249110be90>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type MitigationViewRisksQuery$variables = { + mitigationId: string; +}; +export type MitigationViewRisksQuery$data = { + readonly mitigation: { + readonly id: string; + readonly risks?: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly createdAt: string; + readonly description: string; + readonly id: string; + readonly impact: number; + readonly name: string; + readonly probability: number; + readonly updatedAt: string; + }; + }>; + }; + }; +}; +export type MitigationViewRisksQuery = { + response: MitigationViewRisksQuery$data; + variables: MitigationViewRisksQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "mitigationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "mitigationId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null +}, +v4 = [ + { + "alias": null, + "args": null, + "concreteType": "RiskEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Risk", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "description", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "probability", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "impact", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "updatedAt", + "storageKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "cursor", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "PageInfo", + "kind": "LinkedField", + "name": "pageInfo", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "endCursor", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "hasNextPage", + "storageKey": null + } + ], + "storageKey": null + } +], +v5 = [ + { + "kind": "Literal", + "name": "first", + "value": 100 + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "MitigationViewRisksQuery", + "selections": [ + { + "alias": "mitigation", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": "risks", + "args": null, + "concreteType": "RiskConnection", + "kind": "LinkedField", + "name": "__MitigationView_risks_connection", + "plural": false, + "selections": (v4/*: any*/), + "storageKey": null + } + ], + "type": "Mitigation", + "abstractKey": null + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "MitigationViewRisksQuery", + "selections": [ + { + "alias": "mitigation", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v3/*: any*/), + (v2/*: any*/), + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": (v5/*: any*/), + "concreteType": "RiskConnection", + "kind": "LinkedField", + "name": "risks", + "plural": false, + "selections": (v4/*: any*/), + "storageKey": "risks(first:100)" + }, + { + "alias": null, + "args": (v5/*: any*/), + "filters": null, + "handle": "connection", + "key": "MitigationView_risks", + "kind": "LinkedHandle", + "name": "risks" + } + ], + "type": "Mitigation", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "61c985f00c12fb3cdab3e345a8c65957", + "id": null, + "metadata": { + "connection": [ + { + "count": null, + "cursor": null, + "direction": "forward", + "path": [ + "mitigation", + "risks" + ] + } + ] + }, + "name": "MitigationViewRisksQuery", + "operationKind": "query", + "text": "query MitigationViewRisksQuery(\n $mitigationId: ID!\n) {\n mitigation: node(id: $mitigationId) {\n __typename\n id\n ... on Mitigation {\n risks(first: 100) {\n edges {\n node {\n id\n name\n description\n probability\n impact\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "e05802ec519933edce3d0f3487742e3d"; + +export default node; diff --git a/pkg/coredata/risk.go b/pkg/coredata/risk.go index bc6ec7b4d..e05f5a4b0 100644 --- a/pkg/coredata/risk.go +++ b/pkg/coredata/risk.go @@ -79,7 +79,6 @@ WITH rsks AS ( SELECT id, organization_id, - tenant_id, name, description, probability,