@@ -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
|
||||
|
||||
|
||||
@@ -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<MitigationRisksData | null>(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 (
|
||||
<PageTemplate
|
||||
title={data.mitigation.name ?? ""}
|
||||
@@ -1692,6 +1807,16 @@ function MitigationViewContent({
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="risks" className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Risks
|
||||
{risksData?.mitigation?.risks?.edges &&
|
||||
risksData.mitigation.risks.edges.length > 0 && (
|
||||
<span className="ml-1.5 bg-blue-100 text-blue-800 rounded-full text-xs px-2 py-0.5">
|
||||
{risksData.mitigation.risks.edges.length}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Controls Tab Content */}
|
||||
@@ -2490,6 +2615,88 @@ function MitigationViewContent({
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Risks Tab Content */}
|
||||
<TabsContent value="risks">
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">Risks</h2>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
{risksData?.mitigation?.risks?.edges &&
|
||||
risksData.mitigation.risks.edges.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left font-medium text-sm py-2 px-4">
|
||||
Risk Name
|
||||
</th>
|
||||
<th className="text-left font-medium text-sm py-2 px-4">
|
||||
Probability
|
||||
</th>
|
||||
<th className="text-left font-medium text-sm py-2 px-4">
|
||||
Impact
|
||||
</th>
|
||||
<th className="text-left font-medium text-sm py-2 px-4">
|
||||
Severity
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{risksData.mitigation.risks.edges.map(({ node }) => (
|
||||
<tr
|
||||
key={node.id}
|
||||
className="border-b hover:bg-gray-50"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/organizations/${organizationId}/risks/${node.id}`
|
||||
)
|
||||
}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<td className="py-3 px-4 font-medium text-sm">
|
||||
{node.name}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="bg-blue-100 text-blue-800 px-2 py-0.5 rounded-full text-xs inline-block">
|
||||
{formatProbability(node.probability)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="bg-purple-100 text-purple-800 px-2 py-0.5 rounded-full text-xs inline-block">
|
||||
{formatImpact(node.impact)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div
|
||||
className={`${getRiskSeverityColor(
|
||||
node.probability,
|
||||
node.impact
|
||||
)} px-2 py-0.5 rounded-full text-xs inline-block`}
|
||||
>
|
||||
{getRiskSeverityText(
|
||||
node.probability,
|
||||
node.impact
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No risks linked to this mitigation yet.
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Right task panel */}
|
||||
|
||||
286
apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts
generated
Normal file
286
apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts
generated
Normal file
@@ -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;
|
||||
@@ -79,7 +79,6 @@ WITH rsks AS (
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
tenant_id,
|
||||
name,
|
||||
description,
|
||||
probability,
|
||||
|
||||
Reference in New Issue
Block a user