diff --git a/CHANGELOG.md b/CHANGELOG.md index a3bb5a961..755ccb2ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,17 +16,35 @@ All notable changes to this project will be documented in this file. - New vendor data management system with comprehensive vendor information - Pre-populated vendor database with 12 common SaaS vendors and their certifications - Vendor details page with extended fields for improved vendor management: - - Legal name and headquarters address - - Website URL - - Certification tracking with tag-based interface - - Links to important vendor documents (SLA, DPA, security pages) - - Support for multiple compliance certifications per vendor + - Legal name and headquarters address + - Website URL + - Certification tracking with tag-based interface + - Links to important vendor documents (SLA, DPA, security pages) + - Support for multiple compliance certifications per vendor +- Enhanced risk management with inherent and residual risk assessment capabilities + - Added new fields to track both inherent and residual likelihood/impact values + - Introduced risk severity calculation as the product of likelihood and impact + - Added visual risk matrix to view risk distribution by severity +- New risk-policy mapping functionality allowing risks to be linked to policies +- New risk-control mapping functionality enabling risks to be linked to controls +- Added edit functionality for risks with a new edit page +- Pre-populated risk templates from a JSON data source + +### Changed + +- Updated risk creation form to include both inherent and residual risk parameters +- Improved risk list view with risk matrix visualization +- Enhanced breadcrumb navigation for risk detail pages +- Refactored risk-mitigation mapping to remove redundant probability/impact fields +- Renamed probability field to likelihood for better alignment with risk management terminology ### Fixed - Fix cannot create vendor when the name is too similar to suggested one - Fix UI showing double button to close evidence preview modal - Fix cannot delete vendor with compliance reports (added cascade delete constraint) +- Improved license file formatting in vendors and risks data directories +- Fixed URL in attribution text (`getprobo.com` → `www.getprobo.com`) ## [0.5.0] - 2025-04-10 diff --git a/apps/console/package.json b/apps/console/package.json index 23e52c2e2..902b648dc 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.4", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-tooltip": "^1.1.8", diff --git a/apps/console/public/data/risks/LICENSE.md b/apps/console/public/data/risks/LICENSE.md new file mode 100644 index 000000000..ff161ee4b --- /dev/null +++ b/apps/console/public/data/risks/LICENSE.md @@ -0,0 +1,24 @@ +# Risks Data + +The [risks.json](risks.json) file contains data about various vendors and their security certifications. This data is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. + +## License Requirements + +When using this data, you must: + +1. **Give appropriate credit** - Provide attribution to Probo Inc. and include a link to this license +2. **Indicate if changes were made** - If you modify the data, you must indicate that changes were made +3. **Share under the same license** - If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original + +## Attribution Example + +When using this data, please include the following attribution: + +``` +Data sourced from Probo Inc. (https://www.getprobo.com) under CC BY-SA 4.0 license +``` + +## More Information + +For more information about the CC BY-SA 4.0 license, please visit: +https://creativecommons.org/licenses/by-sa/4.0/ diff --git a/apps/console/public/data/risks/risks.json b/apps/console/public/data/risks/risks.json new file mode 100644 index 000000000..575f0e4cb --- /dev/null +++ b/apps/console/public/data/risks/risks.json @@ -0,0 +1,254 @@ +[ + { + "name": "Data Breach via Phishing", + "description": "Phishing attacks targeting employees can lead to unauthorized access to sensitive data, such as credentials, which can result in a data breach.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.75, + "likelihood": 0.8, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Ransomware Locking Critical Systems", + "description": "Attackers can encrypt critical business data and systems, demanding ransom to restore access. This disrupts operations and can result in financial loss.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 1.0, + "likelihood": 0.3, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Malicious or Negligent employee", + "description": "An employee could leak sensitive data or compromise systems, leading to data loss or operational damage.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.5, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Theft of Intellectual Property (IP)", + "description": "Attackers, including competitors, could steal intellectual property, including designs, algorithms, or trade secrets, harming competitive advantage.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.15, + "recommendedTreatment": "Accept" + } + ] + }, + { + "name": "Exploitation of Unpatched Vulnerabilities", + "description": "Failure to apply security patches on time leaves systems vulnerable to known exploits, allowing attackers to compromise or control critical systems.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.8, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Distributed Denial-of-Service (DDoS) Attack", + "description": "A DDoS attack overwhelms your online services with fake traffic, making them unavailable to customers, causing operational downtime and reputational damage.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.3, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Third-Party Vendor Breach Compromising Startup Data", + "description": "If a third-party vendor with access to your systems is breached, your sensitive data may be exposed due to poor security on the vendor's side.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.1, + "likelihood": 0.15, + "recommendedTreatment": "Accept" + } + ] + }, + { + "name": "Loss of data due to poor or nonexistant Backup Strategy", + "description": "Inadequate or nonexistent backups can lead to permanent data loss, potentially halting operations for extended periods.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.5, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Account Compromise via Weak Authentication", + "description": "Weak passwords or lack of multi-factor authentication (MFA) can lead to unauthorized access to accounts and critical business systems.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.8, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Reputational Damage from Customer Data Leak", + "description": "A breach that exposes sensitive customer data can cause significant reputational damage, reducing trust and potentially leading to lost revenue.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.75, + "likelihood": 0.5, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Regulatory Non-Compliance Leading to Penalties", + "description": "Failure to meet legal standards for data protection (e.g., GDPR) can result in fines and legal action following a breach or violation.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.75, + "likelihood": 0.3, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Fraud via Social Engineering (Impersonation or BEC)", + "description": "Social engineering tactics such as impersonating executives or vendors can trick employees into making fraudulent payments, causing financial loss.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.15, + "recommendedTreatment": "Accept" + } + ] + }, + { + "name": "Device Theft Leading to Data Exposure", + "description": "Loss or theft of employee devices without proper encryption or remote wipe capabilities can result in exposure of sensitive company data.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.25, + "likelihood": 0.5, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Cloud Misconfiguration Causing Data Exposure", + "description": "Misconfigured cloud environments can accidentally expose sensitive data (e.g., public-facing storage buckets), leading to data leaks.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.5, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "API Exploits Leading to Data Breaches", + "description": "Poorly secured or vulnerable APIs can be exploited, allowing attackers to access, manipulate, or steal sensitive data from the backend.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 1.0, + "likelihood": 0.5, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Employee Mishandling of Data Leading to Privacy Violations", + "description": "Untrained or negligent employees could mishandle sensitive data, resulting in accidental exposure or privacy law violations.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.8, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Account Hijacking of Social Media Platforms", + "description": "If attackers hijack your startup's social media accounts, they could post harmful content or engage in fraudulent activities, damaging the brand.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.25, + "likelihood": 0.3, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Weak Encryption Causing Data Exposure", + "description": "Insufficient encryption of sensitive data can lead to exposure in the event of interception or theft, making the data easily accessible to attackers.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.8, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Ineffective change management process", + "description": "Without proper control over changes to systems of softwares, unauthorized or untested changes could introduce vulnerabilities", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.8, + "recommendedTreatment": "Transfer" + } + ] + }, + { + "name": "Insufficient incidient response plan", + "description": "A poorly defined or tested response plan can lead to a slow or inadequate response from the team in reaction to a security breach.", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.25, + "likelihood": 1.0, + "recommendedTreatment": "Reduce" + } + ] + }, + { + "name": "Inadequate logging & monitoring", + "description": "Without proper logging and monitoring, suspicious activities or security incidents may go unnoticed", + "variations": [ + { + "context": "B2B SaaS SMB", + "impact": 0.5, + "likelihood": 0.5, + "recommendedTreatment": "Reduce" + } + ] + } +] diff --git a/apps/console/public/data/vendors/LICENSE.md b/apps/console/public/data/vendors/LICENSE.md index 1b29b58ec..b654718fd 100644 --- a/apps/console/public/data/vendors/LICENSE.md +++ b/apps/console/public/data/vendors/LICENSE.md @@ -1,6 +1,6 @@ # Vendors Data -The `[vendors.json](vendors.json)` file contains data about various vendors and their security certifications. This data is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. +The [vendors.json](vendors.json) file contains data about various vendors and their security certifications. This data is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. ## License Requirements @@ -15,7 +15,7 @@ When using this data, you must: When using this data, please include the following attribution: ``` -Data sourced from Probo Inc. (https://getprobo.com) under CC BY-SA 4.0 license +Data sourced from Probo Inc. (https://www.getprobo.com) under CC BY-SA 4.0 license ``` ## More Information diff --git a/apps/console/src/components/ui/switch.tsx b/apps/console/src/components/ui/switch.tsx new file mode 100644 index 000000000..3a5434ff0 --- /dev/null +++ b/apps/console/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/apps/console/src/pages/organizations/OrganizationBreadcrumb.tsx b/apps/console/src/pages/organizations/OrganizationBreadcrumb.tsx index 68baae0b2..7047ed299 100644 --- a/apps/console/src/pages/organizations/OrganizationBreadcrumb.tsx +++ b/apps/console/src/pages/organizations/OrganizationBreadcrumb.tsx @@ -18,6 +18,7 @@ import { OrganizationBreadcrumbBreadcrumbVendorOverviewQuery } from "./__generat import { OrganizationBreadcrumbOrganizationQuery } from "./__generated__/OrganizationBreadcrumbOrganizationQuery.graphql"; import { OrganizationBreadcrumbBreadcrumbMitigationViewQuery } from "./__generated__/OrganizationBreadcrumbBreadcrumbMitigationViewQuery.graphql"; import { OrganizationBreadcrumbBreadcrumbControlQuery } from "./__generated__/OrganizationBreadcrumbBreadcrumbControlQuery.graphql"; +import { OrganizationBreadcrumbBreadcrumbRiskShowQuery } from "./__generated__/OrganizationBreadcrumbBreadcrumbRiskShowQuery.graphql"; import ErrorBoundary from "@/components/ErrorBoundary"; const New = () => { @@ -418,6 +419,38 @@ function BreadcrumbRiskList() { ); } +function BreadcrumbRiskShow() { + const { organizationId, riskId } = useParams(); + const data = useLazyLoadQuery( + graphql` + query OrganizationBreadcrumbBreadcrumbRiskShowQuery($riskId: ID!) { + risk: node(id: $riskId) { + id + ... on Risk { + name + } + } + } + `, + { riskId: riskId! }, + { fetchPolicy: "store-or-network" } + ); + + return ( + <> + + + + {data.risk?.name} + + + + + ); +} + export function BreadCrumb() { return ( @@ -445,6 +478,16 @@ export function BreadCrumb() { }> } /> + + + + } + > + } /> + }> } /> } /> } /> + } /> } /> } /> diff --git a/apps/console/src/pages/organizations/__generated__/OrganizationBreadcrumbBreadcrumbRiskShowQuery.graphql.ts b/apps/console/src/pages/organizations/__generated__/OrganizationBreadcrumbBreadcrumbRiskShowQuery.graphql.ts new file mode 100644 index 000000000..a84638090 --- /dev/null +++ b/apps/console/src/pages/organizations/__generated__/OrganizationBreadcrumbBreadcrumbRiskShowQuery.graphql.ts @@ -0,0 +1,127 @@ +/** + * @generated SignedSource<<6cc6ac4101b94ac06f94f04368fbaa6d>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type OrganizationBreadcrumbBreadcrumbRiskShowQuery$variables = { + riskId: string; +}; +export type OrganizationBreadcrumbBreadcrumbRiskShowQuery$data = { + readonly risk: { + readonly id: string; + readonly name?: string; + }; +}; +export type OrganizationBreadcrumbBreadcrumbRiskShowQuery = { + response: OrganizationBreadcrumbBreadcrumbRiskShowQuery$data; + variables: OrganizationBreadcrumbBreadcrumbRiskShowQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "riskId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "riskId" + } +], +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v3 = { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "Risk", + "abstractKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "OrganizationBreadcrumbBreadcrumbRiskShowQuery", + "selections": [ + { + "alias": "risk", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "OrganizationBreadcrumbBreadcrumbRiskShowQuery", + "selections": [ + { + "alias": "risk", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "eaafff390efc7cb4b97300fa65d98c99", + "id": null, + "metadata": {}, + "name": "OrganizationBreadcrumbBreadcrumbRiskShowQuery", + "operationKind": "query", + "text": "query OrganizationBreadcrumbBreadcrumbRiskShowQuery(\n $riskId: ID!\n) {\n risk: node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "3f83f754047443e38a51bd8d2351f81c"; + +export default node; diff --git a/apps/console/src/pages/organizations/mitigations/MitigationListView.tsx b/apps/console/src/pages/organizations/mitigations/MitigationListView.tsx index ced6cde4d..29a0402b5 100644 --- a/apps/console/src/pages/organizations/mitigations/MitigationListView.tsx +++ b/apps/console/src/pages/organizations/mitigations/MitigationListView.tsx @@ -16,6 +16,8 @@ import { CheckCircle2, Clock, X, + HelpCircle, + ExternalLink, } from "lucide-react"; import { PageTemplate } from "@/components/PageTemplate"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -24,6 +26,17 @@ import { MitigationListViewSkeleton } from "./MitigationListPage"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { MitigationListViewImportMitigationMutation as MitigationListViewImportMitigationMutationType } from "./__generated__/MitigationListViewImportMitigationMutation.graphql"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; const mitigationListViewQuery = graphql` query MitigationListViewQuery($organizationId: ID!, $first: Int) { @@ -543,12 +556,113 @@ function MitigationListContent({ - - {mitigation.name} - +
+ + + + + + {mitigation.name} + + + + + +

+ Click to view details +

+
+
+
+ + {/* Enhanced popover for description info */} + + + + + +
+
+

+ {mitigation.name} +

+ + {mitigation.importance} + +
+ + {mitigation.description && ( +
+

+ Why is this important: +

+

+ {mitigation.description.startsWith( + "##" + ) + ? mitigation.description + .split("\n") + .find((line) => + line.startsWith( + "## Why" + ) + ) + ?.replace("## Why?", "") + ?.replace("## Why", "") + ?.trim() || + mitigation.description.split( + "\n" + )[1] || + "" + : mitigation.description.substring( + 0, + 180 + ) + + (mitigation.description + .length > 180 + ? "..." + : "")} +

+
+ )} + +
+ +
+
+
+
+
))} diff --git a/apps/console/src/pages/organizations/mitigations/MitigationView.tsx b/apps/console/src/pages/organizations/mitigations/MitigationView.tsx index 5e18982d1..2c8a04c59 100644 --- a/apps/console/src/pages/organizations/mitigations/MitigationView.tsx +++ b/apps/console/src/pages/organizations/mitigations/MitigationView.tsx @@ -449,8 +449,8 @@ const mitigationRisksQuery = graphql` id name description - probability - impact + inherentLikelihood + inherentImpact createdAt updatedAt } @@ -491,8 +491,9 @@ type RiskNode = { id: string; name: string; description: string; - probability: number; + likelihood: number; impact: number; + severity: number; createdAt: string; updatedAt: string; }; @@ -1846,12 +1847,7 @@ function MitigationViewContent({ }, [controlSearchQuery, getControls]); // Helper function to get risk severity color - const getRiskSeverityColor = ( - probability: number, - impact: number - ): string => { - const severity = probability * impact; - + const getRiskSeverityColor = (severity: number): string => { 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"; @@ -1859,17 +1855,15 @@ function MitigationViewContent({ }; // Helper function to get risk severity text - const getRiskSeverityText = (probability: number, impact: number): string => { - const severity = probability * impact; - + const getRiskSeverityText = (severity: number): string => { 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 => { + // Format likelihood as text + const formatlikelihood = (value: number): string => { if (value <= 0.1) return "Very Low"; if (value <= 0.3) return "Low"; if (value <= 0.5) return "Medium"; @@ -2825,7 +2819,7 @@ function MitigationViewContent({ Risk Name - Probability + likelihood Impact @@ -2852,7 +2846,7 @@ function MitigationViewContent({
- {formatProbability(node.probability)} + {formatlikelihood(node.likelihood)}
@@ -2863,14 +2857,10 @@ function MitigationViewContent({
- {getRiskSeverityText( - node.probability, - node.impact - )} + {getRiskSeverityText(node.severity)}
diff --git a/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts b/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts index a4396efcf..746071822 100644 --- a/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts +++ b/apps/console/src/pages/organizations/mitigations/__generated__/MitigationViewRisksQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7ecc796e34bf88aa378e81249110be90>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -21,9 +21,9 @@ export type MitigationViewRisksQuery$data = { readonly createdAt: string; readonly description: string; readonly id: string; - readonly impact: number; + readonly inherentImpact: number; + readonly inherentLikelihood: number; readonly name: string; - readonly probability: number; readonly updatedAt: string; }; }>; @@ -100,14 +100,14 @@ v4 = [ "alias": null, "args": null, "kind": "ScalarField", - "name": "probability", + "name": "inherentLikelihood", "storageKey": null }, { "alias": null, "args": null, "kind": "ScalarField", - "name": "impact", + "name": "inherentImpact", "storageKey": null }, { @@ -259,7 +259,7 @@ return { ] }, "params": { - "cacheID": "61c985f00c12fb3cdab3e345a8c65957", + "cacheID": "6e8982bef70e90ee745d6985c82c597e", "id": null, "metadata": { "connection": [ @@ -276,11 +276,11 @@ return { }, "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" + "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 inherentLikelihood\n inherentImpact\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"; +(node as any).hash = "0fe825c558627da60c1025efa825f91e"; export default node; diff --git a/apps/console/src/pages/organizations/risks/EditRiskPage.tsx b/apps/console/src/pages/organizations/risks/EditRiskPage.tsx new file mode 100644 index 000000000..061fce0ef --- /dev/null +++ b/apps/console/src/pages/organizations/risks/EditRiskPage.tsx @@ -0,0 +1,61 @@ +import { Suspense } from "react"; +import { useLocation } from "react-router"; +import { ErrorBoundaryWithLocation } from "../ErrorBoundary"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { PageTemplate } from "@/components/PageTemplate"; +import { EditRiskView } from "./EditRiskView"; + +// We'll import this once we create it +// import { EditRiskView } from "./EditRiskView"; + +export function EditRiskViewSkeleton() { + return ( + + + + + + + +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ ); +} + +export function EditRiskPage() { + const location = useLocation(); + + return ( + }> + + + + + ); +} + +export default EditRiskPage; diff --git a/apps/console/src/pages/organizations/risks/EditRiskView.tsx b/apps/console/src/pages/organizations/risks/EditRiskView.tsx new file mode 100644 index 000000000..5cb58f77f --- /dev/null +++ b/apps/console/src/pages/organizations/risks/EditRiskView.tsx @@ -0,0 +1,404 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router"; +import { + graphql, + useMutation, + usePreloadedQuery, + useQueryLoader, + PreloadedQuery, +} from "react-relay"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { useToast } from "@/hooks/use-toast"; +import { PageTemplate } from "@/components/PageTemplate"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Suspense } from "react"; +import { EditRiskViewSkeleton } from "./EditRiskPage"; +import type { EditRiskViewQuery } from "./__generated__/EditRiskViewQuery.graphql"; +import type { EditRiskViewUpdateRiskMutation } from "./__generated__/EditRiskViewUpdateRiskMutation.graphql"; + +// Query to get risk details +const editRiskViewQuery = graphql` + query EditRiskViewQuery($riskId: ID!) { + risk: node(id: $riskId) { + ... on Risk { + id + name + description + inherentLikelihood + inherentImpact + residualLikelihood + residualImpact + } + } + } +`; + +// Mutation to update risk +const updateRiskMutation = graphql` + mutation EditRiskViewUpdateRiskMutation($input: UpdateRiskInput!) { + updateRisk(input: $input) { + risk { + id + name + description + inherentLikelihood + inherentImpact + residualLikelihood + residualImpact + updatedAt + } + } + } +`; + +// Component to handle the actual editing once data is loaded +function EditRiskViewContent({ + queryRef, +}: { + queryRef: PreloadedQuery; +}) { + const navigate = useNavigate(); + const { organizationId, riskId } = useParams<{ + organizationId: string; + riskId: string; + }>(); + const { toast } = useToast(); + + const data = usePreloadedQuery(editRiskViewQuery, queryRef); + const risk = data.risk; + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [inherentLikelihood, setInherentLikelihood] = + useState("MEDIUM"); + const [inherentImpact, setInherentImpact] = useState("MEDIUM"); + const [residualLikelihood, setResidualLikelihood] = + useState("MEDIUM"); + const [residualImpact, setResidualImpact] = useState("MEDIUM"); + const [isSubmitting, setIsSubmitting] = useState(false); + + const [updateRisk, isInFlight] = + useMutation(updateRiskMutation); + + // Helper function to convert float to likelihood string + const floatToLikelihood = (value: number): string => { + if (value <= 0.2) return "VERY_LOW"; + if (value <= 0.4) return "LOW"; + if (value <= 0.6) return "MEDIUM"; + if (value <= 0.8) return "HIGH"; + return "VERY_HIGH"; + }; + + // Helper function to convert float to impact string + const floatToImpact = (value: number): string => { + if (value <= 0.2) return "VERY_LOW"; + if (value <= 0.4) return "LOW"; + if (value <= 0.6) return "MEDIUM"; + if (value <= 0.8) return "HIGH"; + return "VERY_HIGH"; + }; + + // Map string values to float values + const likelihoodToFloat = (value: string): number => { + switch (value) { + case "VERY_LOW": + return 0.1; + case "LOW": + return 0.3; + case "MEDIUM": + return 0.5; + case "HIGH": + return 0.7; + case "VERY_HIGH": + return 0.9; + default: + return 0.5; + } + }; + + const impactToFloat = (value: string): number => { + switch (value) { + case "VERY_LOW": + return 0.1; + case "LOW": + return 0.3; + case "MEDIUM": + return 0.5; + case "HIGH": + return 0.7; + case "VERY_HIGH": + return 0.9; + default: + return 0.5; + } + }; + + // Initialize form with risk data + useEffect(() => { + if (risk) { + setName(risk.name || ""); + setDescription(risk.description || ""); + setInherentLikelihood(floatToLikelihood(risk.inherentLikelihood || 0.5)); + setInherentImpact(floatToImpact(risk.inherentImpact || 0.5)); + setResidualLikelihood(floatToLikelihood(risk.residualLikelihood || 0.5)); + setResidualImpact(floatToImpact(risk.residualImpact || 0.5)); + } + }, [risk]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!name.trim()) { + toast({ + title: "Error", + description: "Please enter a name for the risk.", + variant: "destructive", + }); + return; + } + + setIsSubmitting(true); + + const input = { + id: riskId!, + name, + description, + inherentLikelihood: likelihoodToFloat(inherentLikelihood), + inherentImpact: impactToFloat(inherentImpact), + residualLikelihood: likelihoodToFloat(residualLikelihood), + residualImpact: impactToFloat(residualImpact), + }; + + updateRisk({ + variables: { + input, + }, + onCompleted: (response, errors) => { + setIsSubmitting(false); + if (errors) { + console.error("Error updating risk:", errors); + toast({ + title: "Error", + description: "Failed to update risk. Please try again.", + variant: "destructive", + }); + return; + } + + toast({ + title: "Success", + description: "Risk updated successfully!", + }); + + navigate(`/organizations/${organizationId}/risks/${riskId}`); + }, + onError: (error) => { + setIsSubmitting(false); + console.error("Error updating risk:", error); + toast({ + title: "Error", + description: "Failed to update risk. Please try again.", + variant: "destructive", + }); + }, + }); + }; + + return ( + + + + Risk Details + Update the details of this risk + + +
+
+ + setName(e.target.value)} + required + /> +
+ +
+ +