Refactor risk management

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-04-12 00:18:31 -07:00
parent c5e7f030a8
commit 16efab774a
48 changed files with 5725 additions and 779 deletions

View File

@@ -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

View File

@@ -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",

View File

@@ -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/

View File

@@ -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"
}
]
}
]

View File

@@ -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

View File

@@ -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<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View File

@@ -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<OrganizationBreadcrumbBreadcrumbRiskShowQuery>(
graphql`
query OrganizationBreadcrumbBreadcrumbRiskShowQuery($riskId: ID!) {
risk: node(id: $riskId) {
id
... on Risk {
name
}
}
}
`,
{ riskId: riskId! },
{ fetchPolicy: "store-or-network" }
);
return (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbNavLink
to={`/organizations/${organizationId}/risks/${riskId}`}
>
{data.risk?.name}
</BreadcrumbNavLink>
</BreadcrumbItem>
<Outlet />
</>
);
}
export function BreadCrumb() {
return (
<Routes>
@@ -445,6 +478,16 @@ export function BreadCrumb() {
</Route>
<Route path="risks" element={<BreadcrumbRiskList />}>
<Route path="new" element={<New />} />
<Route
path=":riskId"
element={
<Suspense>
<BreadcrumbRiskShow />
</Suspense>
}
>
<Route path="edit" element={<Edit />} />
</Route>
</Route>
<Route path="frameworks" element={<BreadcrumbFrameworkList />}>
<Route

View File

@@ -22,6 +22,7 @@ import { EditPolicyPage } from "./policies/EditPolicyPage";
import { NewPolicyPage } from "./policies/NewPolicyPage";
import { PolicyListPage } from "./policies/PolicyListPage";
import { PolicyPage } from "./policies/PolicyPage";
import { EditRiskPage } from "./risks/EditRiskPage";
import { NewRiskPage } from "./risks/NewRiskPage";
import { RiskListPage } from "./risks/RiskListPage";
import ShowRiskView from "./risks/ShowRiskView";
@@ -62,6 +63,7 @@ export function OrganizationsRoutes() {
<Route path="risks" element={<RiskListPage />} />
<Route path="risks/new" element={<NewRiskPage />} />
<Route path="risks/:riskId" element={<ShowRiskView />} />
<Route path="risks/:riskId/edit" element={<EditRiskPage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>

View File

@@ -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;

View File

@@ -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({
</div>
</td>
<td className="px-4 py-3 align-middle">
<Link
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
className="font-medium block"
>
{mitigation.name}
</Link>
<div className="flex items-center gap-3">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
className="font-medium group flex items-center relative"
>
<span className="mr-1">
{mitigation.name}
</span>
<span className="absolute bottom-0 left-0 w-full h-0.5 bg-primary-bg scale-x-0 group-hover:scale-x-100 transition-transform"></span>
</Link>
</TooltipTrigger>
<TooltipContent side="bottom">
<p className="text-xs">
Click to view details
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* Enhanced popover for description info */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
className="p-0 h-6 w-6 rounded-full hover:bg-primary-bg"
aria-label="Learn more about this mitigation"
>
<HelpCircle className="h-5 w-5 text-primary hover:text-primary" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-80 p-4 shadow-md"
align="start"
sideOffset={5}
>
<div className="space-y-3">
<div className="flex justify-between items-start">
<h4 className="font-bold text-md">
{mitigation.name}
</h4>
<Badge
variant="outline"
className="text-xs"
>
{mitigation.importance}
</Badge>
</div>
{mitigation.description && (
<div className="text-sm">
<p className="font-semibold mb-1">
Why is this important:
</p>
<p>
{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
? "..."
: "")}
</p>
</div>
)}
<div className="pt-2 flex justify-end">
<Button
variant="outline"
size="sm"
className="text-xs gap-1"
asChild
>
<Link
to={`/organizations/${organizationId}/mitigations/${mitigation.id}`}
>
<span>
Implementation guide
</span>
<ExternalLink className="h-3 w-3" />
</Link>
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
</td>
</tr>
))}

View File

@@ -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
</th>
<th className="text-left font-medium text-sm py-2 px-4">
Probability
likelihood
</th>
<th className="text-left font-medium text-sm py-2 px-4">
Impact
@@ -2852,7 +2846,7 @@ function MitigationViewContent({
</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)}
{formatlikelihood(node.likelihood)}
</div>
</td>
<td className="py-3 px-4">
@@ -2863,14 +2857,10 @@ function MitigationViewContent({
<td className="py-3 px-4">
<div
className={`${getRiskSeverityColor(
node.probability,
node.impact
node.severity
)} px-2 py-0.5 rounded-full text-xs inline-block`}
>
{getRiskSeverityText(
node.probability,
node.impact
)}
{getRiskSeverityText(node.severity)}
</div>
</td>
</tr>

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<7ecc796e34bf88aa378e81249110be90>>
* @generated SignedSource<<b7154e691d26fef3b8f64ec3a39304fd>>
* @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;

View File

@@ -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 (
<PageTemplate title="Edit Risk">
<Card className="max-w-2xl">
<CardHeader>
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-96 mt-2" />
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-24 w-full" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<div className="flex justify-end gap-3">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</CardContent>
</Card>
</PageTemplate>
);
}
export function EditRiskPage() {
const location = useLocation();
return (
<Suspense key={location.pathname} fallback={<EditRiskViewSkeleton />}>
<ErrorBoundaryWithLocation>
<EditRiskView />
</ErrorBoundaryWithLocation>
</Suspense>
);
}
export default EditRiskPage;

View File

@@ -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<EditRiskViewQuery>;
}) {
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<string>("MEDIUM");
const [inherentImpact, setInherentImpact] = useState<string>("MEDIUM");
const [residualLikelihood, setResidualLikelihood] =
useState<string>("MEDIUM");
const [residualImpact, setResidualImpact] = useState<string>("MEDIUM");
const [isSubmitting, setIsSubmitting] = useState(false);
const [updateRisk, isInFlight] =
useMutation<EditRiskViewUpdateRiskMutation>(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 (
<PageTemplate
title="Edit Risk"
description="Update the details of this risk"
>
<Card className="max-w-2xl">
<CardHeader>
<CardTitle>Risk Details</CardTitle>
<CardDescription>Update the details of this risk</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
placeholder="Risk name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe the risk in detail"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={5}
/>
</div>
<Separator />
<div>
<h3 className="text-lg font-medium mb-2">
Initial Risk Assessment
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="inherentLikelihood">
Inherent Likelihood
</Label>
<Select
value={inherentLikelihood}
onValueChange={setInherentLikelihood}
>
<SelectTrigger id="inherentLikelihood">
<SelectValue placeholder="Select likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="inherentImpact">Inherent Impact</Label>
<Select
value={inherentImpact}
onValueChange={setInherentImpact}
>
<SelectTrigger id="inherentImpact">
<SelectValue placeholder="Select impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<Separator />
<div>
<h3 className="text-lg font-medium mb-2">Residual Risk</h3>
<p className="text-sm text-tertiary mb-4">
Estimate the risk after treatment measures have been applied
</p>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residualLikelihood">
Residual Likelihood
</Label>
<Select
value={residualLikelihood}
onValueChange={setResidualLikelihood}
>
<SelectTrigger id="residualLikelihood">
<SelectValue placeholder="Select residual likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="residualImpact">Residual Impact</Label>
<Select
value={residualImpact}
onValueChange={setResidualImpact}
>
<SelectTrigger id="residualImpact">
<SelectValue placeholder="Select residual impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex justify-end gap-3">
<Button
type="button"
variant="outline"
onClick={() =>
navigate(`/organizations/${organizationId}/risks/${riskId}`)
}
>
Cancel
</Button>
<Button type="submit" disabled={isInFlight || isSubmitting}>
{isInFlight || isSubmitting ? "Updating..." : "Update Risk"}
</Button>
</div>
</form>
</CardContent>
</Card>
</PageTemplate>
);
}
// Main component that loads the query
export function EditRiskView() {
const { riskId } = useParams<{ riskId: string }>();
const [queryRef, loadQuery] =
useQueryLoader<EditRiskViewQuery>(editRiskViewQuery);
useEffect(() => {
if (riskId) {
loadQuery({ riskId });
}
}, [loadQuery, riskId]);
if (!queryRef) {
return <EditRiskViewSkeleton />;
}
return (
<Suspense fallback={<EditRiskViewSkeleton />}>
<EditRiskViewContent queryRef={queryRef} />
</Suspense>
);
}
export default EditRiskView;

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router";
import { ConnectionHandler, graphql, useMutation } from "react-relay";
import {
@@ -21,74 +21,18 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
// Risk template library - static definitions of common risks
const riskTemplates = [
{
id: "data-breach",
name: "Data Breach",
description:
"Unauthorized access to sensitive data resulting in data disclosure, theft, or corruption.",
probability: "HIGH",
impact: "HIGH",
},
{
id: "service-outage",
name: "Service Outage",
description:
"System downtime or degradation affecting availability of services to customers.",
probability: "MEDIUM",
impact: "HIGH",
},
{
id: "compliance-violation",
name: "Compliance Violation",
description:
"Failure to meet regulatory requirements resulting in penalties or legal action.",
probability: "MEDIUM",
impact: "VERY_HIGH",
},
{
id: "insider-threat",
name: "Insider Threat",
description:
"Malicious actions by employees or contractors with privileged access to systems or data.",
probability: "LOW",
impact: "HIGH",
},
{
id: "third-party-risk",
name: "Third-Party Risk",
description:
"Vulnerabilities introduced through vendors, suppliers, or partners with access to systems or data.",
probability: "MEDIUM",
impact: "MEDIUM",
},
{
id: "ransomware",
name: "Ransomware Attack",
description:
"Malware that encrypts data and demands payment for decryption keys.",
probability: "MEDIUM",
impact: "VERY_HIGH",
},
{
id: "ddos",
name: "DDoS Attack",
description:
"Distributed denial of service attack overwhelming systems and preventing legitimate access.",
probability: "MEDIUM",
impact: "HIGH",
},
{
id: "credential-compromise",
name: "Credential Compromise",
description:
"Unauthorized access to accounts due to weak, stolen, or improperly secured credentials.",
probability: "HIGH",
impact: "HIGH",
},
];
interface RiskTemplate {
name: string;
description: string;
variations: {
context: string;
impact: number;
likelihood: number;
recommendedTreatment: string;
}[];
}
const createRiskMutation = graphql`
mutation NewRiskViewCreateRiskMutation(
@@ -101,8 +45,10 @@ const createRiskMutation = graphql`
id
name
description
probability
impact
inherentLikelihood
inherentImpact
residualLikelihood
residualImpact
createdAt
updatedAt
}
@@ -116,16 +62,40 @@ export default function NewRiskView() {
const { organizationId } = useParams<{ organizationId: string }>();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [probability, setProbability] = useState<string>("MEDIUM");
const [impact, setImpact] = useState<string>("MEDIUM");
const [inherentLikelihood, setinherentLikelihood] =
useState<string>("MEDIUM");
const [inherentImpact, setinherentImpact] = useState<string>("MEDIUM");
const [residualLikelihood, setResidualLikelihood] =
useState<string>("MEDIUM");
const [residualImpact, setResidualImpact] = useState<string>("MEDIUM");
const [isSubmitting, setIsSubmitting] = useState(false);
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
const [riskTemplates, setRiskTemplates] = useState<RiskTemplate[]>([]);
const { toast } = useToast();
const [commitMutation, isInFlight] = useMutation(createRiskMutation);
const [createRisk, isInFlight] = useMutation(createRiskMutation);
useEffect(() => {
const loadRiskTemplates = async () => {
try {
const response = await fetch("/data/risks/risks.json");
const data = await response.json();
setRiskTemplates(data);
} catch (error) {
console.error("Error loading risk templates:", error);
toast({
title: "Error",
description: "Failed to load risk templates. Please try again.",
variant: "destructive",
});
}
};
loadRiskTemplates();
}, [toast]);
// Map string values to float values
const probabilityToFloat = (value: string): number => {
const likelihoodToFloat = (value: string): number => {
switch (value) {
case "VERY_LOW":
return 0.1;
@@ -167,20 +137,48 @@ export default function NewRiskView() {
// Clear form if "Select a template" is chosen
setName("");
setDescription("");
setProbability("MEDIUM");
setImpact("MEDIUM");
setinherentLikelihood("MEDIUM");
setinherentImpact("MEDIUM");
setResidualLikelihood("MEDIUM");
setResidualImpact("MEDIUM");
return;
}
const template = riskTemplates.find((t) => t.id === templateId);
const template = riskTemplates[parseInt(templateId)];
if (template) {
setName(template.name);
setDescription(template.description);
setProbability(template.probability);
setImpact(template.impact);
// Convert numeric values to string values for the select components
const likelihoodValue = floatTolikelihood(
template.variations[0].likelihood
);
const impactValue = floatToImpact(template.variations[0].impact);
setinherentLikelihood(likelihoodValue);
setinherentImpact(impactValue);
// Set residual values to be the same as initial values by default
setResidualLikelihood(likelihoodValue);
setResidualImpact(impactValue);
}
};
// Helper function to convert float likelihood to 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 impact to 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";
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
@@ -199,11 +197,13 @@ export default function NewRiskView() {
organizationId: organizationId!,
name,
description,
probability: probabilityToFloat(probability),
impact: impactToFloat(impact),
inherentLikelihood: likelihoodToFloat(inherentLikelihood),
inherentImpact: impactToFloat(inherentImpact),
residualLikelihood: likelihoodToFloat(residualLikelihood),
residualImpact: impactToFloat(residualImpact),
};
commitMutation({
createRisk({
variables: {
input,
connections: [
@@ -268,10 +268,10 @@ export default function NewRiskView() {
<SelectTrigger id="template">
<SelectValue placeholder="Select a risk template" />
</SelectTrigger>
<SelectContent>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="none">Select a template</SelectItem>
{riskTemplates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{riskTemplates.map((template, index) => (
<SelectItem key={index} value={index.toString()}>
{template.name}
</SelectItem>
))}
@@ -306,12 +306,15 @@ export default function NewRiskView() {
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="probability">Probability</Label>
<Select value={probability} onValueChange={setProbability}>
<SelectTrigger id="probability">
<SelectValue placeholder="Select probability" />
<Label htmlFor="inherentLikelihood">Initial Likelihood</Label>
<Select
value={inherentLikelihood}
onValueChange={setinherentLikelihood}
>
<SelectTrigger id="inherentLikelihood">
<SelectValue placeholder="Select inherentLikelihood" />
</SelectTrigger>
<SelectContent>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
@@ -322,12 +325,15 @@ export default function NewRiskView() {
</div>
<div className="space-y-2">
<Label htmlFor="impact">Impact</Label>
<Select value={impact} onValueChange={setImpact}>
<SelectTrigger id="impact">
<SelectValue placeholder="Select impact" />
<Label htmlFor="inherentImpact">Initial Impact</Label>
<Select
value={inherentImpact}
onValueChange={setinherentImpact}
>
<SelectTrigger id="inherentImpact">
<SelectValue placeholder="Select inherentImpact" />
</SelectTrigger>
<SelectContent>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
@@ -338,6 +344,57 @@ export default function NewRiskView() {
</div>
</div>
<Separator className="my-4" />
<div>
<h3 className="text-lg font-medium mb-2">Residual Risk</h3>
<p className="text-sm text-tertiary mb-4">
Estimate the risk after treatment measures have been applied
</p>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="residualLikelihood">
Residual Likelihood
</Label>
<Select
value={residualLikelihood}
onValueChange={setResidualLikelihood}
>
<SelectTrigger id="residualLikelihood">
<SelectValue placeholder="Select residual likelihood" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="residualImpact">Residual Impact</Label>
<Select
value={residualImpact}
onValueChange={setResidualImpact}
>
<SelectTrigger id="residualImpact">
<SelectValue placeholder="Select residual impact" />
</SelectTrigger>
<SelectContent className="max-h-[300px] overflow-y-auto">
<SelectItem value="VERY_LOW">Very Low</SelectItem>
<SelectItem value="LOW">Low</SelectItem>
<SelectItem value="MEDIUM">Medium</SelectItem>
<SelectItem value="HIGH">High</SelectItem>
<SelectItem value="VERY_HIGH">Very High</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex justify-end gap-3">
<Button
type="button"

View File

@@ -22,9 +22,9 @@ import { RiskViewSkeleton } from "./RiskListPage";
import { RiskListViewPaginationQuery } from "./__generated__/RiskListViewPaginationQuery.graphql";
import { RiskListView_risks$key } from "./__generated__/RiskListView_risks.graphql";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Link } from "react-router";
import { Plus, Trash2 } from "lucide-react";
import { Plus, Trash2, Edit } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import {
Dialog,
@@ -35,6 +35,13 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { RiskListViewDeleteMutation } from "./__generated__/RiskListViewDeleteMutation.graphql";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
const defaultPageSize = 25;
@@ -71,8 +78,10 @@ const riskListFragment = graphql`
node {
id
name
probability
impact
inherentLikelihood
inherentImpact
residualLikelihood
residualImpact
description
createdAt
updatedAt
@@ -99,21 +108,9 @@ const deleteRiskMutation = graphql`
}
`;
// Helper functions to convert float values to text
const floatToProbabilityText = (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";
};
const floatToImpactText = (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";
// Helper function to convert float values to percentage
const floatToPercentage = (value: number): string => {
return `${Math.round(value * 100)}%`;
};
function LoadAboveButton({
@@ -170,6 +167,228 @@ function LoadBelowButton({
);
}
// Define colors for risk matrix cells
const riskMatrixColors = {
lowest: "bg-green-500 text-white",
low: "bg-lime-300 text-black",
medium: "bg-yellow-300 text-black",
high: "bg-amber-400 text-white",
highest: "bg-red-500 text-white",
};
// Empty cell variants (lighter colors)
const emptyRiskMatrixColors = {
lowest: "bg-green-50 text-black",
low: "bg-lime-50 text-black",
medium: "bg-yellow-50 text-black",
high: "bg-amber-50 text-black",
highest: "bg-red-50 text-black",
};
// Risk Matrix Component
function RiskMatrix({
risks,
isResidual = false,
}: {
risks: Array<{
id: string;
name: string;
inherentLikelihood: number;
inherentImpact: number;
residualLikelihood?: number;
residualImpact?: number;
}>;
isResidual?: boolean;
}): JSX.Element {
// Define likelihood and impact ranges for the 5x5 matrix
const likelihoodRanges: [number, number][] = [
[0.8, 1], // Highest likelihood
[0.6, 0.8], // High likelihood
[0.4, 0.6], // Medium likelihood
[0.2, 0.4], // Low likelihood
[0, 0.2], // Lowest likelihood
];
const impactRanges: [number, number][] = [
[0, 0.2], // Lowest impact
[0.2, 0.4], // Low impact
[0.4, 0.6], // Medium impact
[0.6, 0.8], // High impact
[0.8, 1], // Highest impact
];
const likelihoodLabels = ["Very High", "High", "Medium", "Low", "Very Low"];
const impactLabels = ["Very Low", "Low", "Medium", "High", "Very High"];
// Function to get cell content with risks that fall in this cell
const getCellContent = (
likelihoodRange: [number, number],
impactRange: [number, number]
) => {
return risks.filter((risk) => {
const likelihood = isResidual
? risk.residualLikelihood ?? risk.inherentLikelihood
: risk.inherentLikelihood;
const impact = isResidual
? risk.residualImpact ?? risk.inherentImpact
: risk.inherentImpact;
return (
likelihood > likelihoodRange[0] &&
likelihood <= likelihoodRange[1] &&
impact > impactRange[0] &&
impact <= impactRange[1]
);
});
};
// Helper to determine cell color based on position in matrix
// New matrix has rows indexed from top to bottom (0 = highest likelihood, 4 = lowest likelihood)
// and columns indexed from left to right (0 = lowest impact, 4 = highest impact)
const getCellColor = (row: number, col: number, isEmpty: boolean): string => {
const colorSet = isEmpty ? emptyRiskMatrixColors : riskMatrixColors;
// Top row (highest likelihood)
if (row === 0) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.high;
return colorSet.highest;
}
// Second row
if (row === 1) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.medium;
if (col === 2) return colorSet.high;
return colorSet.highest;
}
// Middle row
if (row === 2) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.low;
if (col === 2) return colorSet.medium;
if (col === 3) return colorSet.high;
return colorSet.highest;
}
// Fourth row
if (row === 3) {
if (col === 0) return colorSet.lowest;
if (col === 1) return colorSet.low;
if (col === 2 || col === 3) return colorSet.medium;
return colorSet.high;
}
// Bottom row (lowest likelihood)
if (row === 4) {
if (col <= 1) return colorSet.lowest;
if (col <= 3) return colorSet.low;
return colorSet.medium;
}
return "bg-gray-100";
};
// Instead of using refs and tippy, we'll use a component with popover
const RiskCell = ({
rowIndex,
colIndex,
likelihoodRange,
impactRange,
}: {
rowIndex: number;
colIndex: number;
likelihoodRange: [number, number];
impactRange: [number, number];
}) => {
const cellRisks = getCellContent(likelihoodRange, impactRange);
const isEmpty = cellRisks.length === 0;
const cellColor = getCellColor(rowIndex, colIndex, isEmpty);
if (isEmpty) {
return (
<td
className={`border aspect-square w-14 h-14 text-center ${cellColor}`}
data-risks={0}
>
<div className="text-sm font-bold flex items-center justify-center h-full"></div>
</td>
);
}
return (
<Popover>
<PopoverTrigger asChild>
<td
className={`border aspect-square w-14 h-14 text-center ${cellColor} cursor-pointer hover:opacity-90`}
data-risks={cellRisks.length}
>
<div className="text-sm font-bold flex items-center justify-center h-full">
{cellRisks.length}
</div>
</td>
</PopoverTrigger>
<PopoverContent className="w-64 p-3" align="center">
<div className="font-medium mb-2">
{cellRisks.length} Risk{cellRisks.length > 1 ? "s" : ""}:
</div>
<ul className="list-disc pl-4 space-y-1">
{cellRisks.map((risk) => (
<li key={risk.id} className="text-sm">
{risk.name}
</li>
))}
</ul>
</PopoverContent>
</Popover>
);
};
return (
<div className="overflow-x-auto">
<div className="flex flex-col">
<div className="text-xs font-medium text-center mb-1">CONSEQUENCE</div>
<table className="w-full border-collapse table-fixed">
<thead>
<tr>
<th className="p-1 text-center border w-14"></th>
{impactLabels.map((label, index) => (
<th
key={index}
className="p-1 text-xs text-center border font-medium w-14"
>
{label}
</th>
))}
</tr>
</thead>
<tbody>
{likelihoodRanges.map((likelihoodRange, rowIndex) => (
<tr key={rowIndex}>
<th className="p-1 text-xs text-center border font-medium w-14 h-14">
{likelihoodLabels[rowIndex]}
</th>
{impactRanges.map((impactRange, colIndex) => (
<RiskCell
key={colIndex}
rowIndex={rowIndex}
colIndex={colIndex}
likelihoodRange={likelihoodRange}
impactRange={impactRange}
/>
))}
</tr>
))}
</tbody>
</table>
<div className="text-xs font-medium ml-2 mt-1">LIKELIHOOD</div>
</div>
</div>
);
}
function RiskListViewContent({
queryRef,
}: {
@@ -188,6 +407,9 @@ function RiskListViewContent({
} | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
// State for toggling between initial and residual risk matrix
const [showResidualRisk, setShowResidualRisk] = useState(false);
// Setup delete mutation
const [commitDeleteMutation] =
useMutation<RiskListViewDeleteMutation>(deleteRiskMutation);
@@ -282,6 +504,46 @@ function RiskListViewContent({
}}
/>
{/* Combined Risk Matrix with Toggle */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>
{showResidualRisk
? "Residual Risk Matrix"
: "Initial Risk Matrix"}
</CardTitle>
<div className="flex items-center space-x-4 border-2 border-gray-300 rounded-lg p-3 bg-gray-50 shadow-md">
<Label
htmlFor="risk-toggle"
className={`text-sm font-semibold cursor-pointer ${
!showResidualRisk ? "text-primary" : "text-muted-foreground"
}`}
>
Initial
</Label>
<div className="relative">
<Switch
id="risk-toggle"
checked={showResidualRisk}
onCheckedChange={setShowResidualRisk}
className="border-2 border-gray-400 data-[state=checked]:border-primary"
/>
</div>
<Label
htmlFor="risk-toggle"
className={`text-sm font-semibold cursor-pointer ${
showResidualRisk ? "text-primary" : "text-muted-foreground"
}`}
>
Residual
</Label>
</div>
</CardHeader>
<CardContent>
<RiskMatrix risks={risks} isResidual={showResidualRisk} />
</CardContent>
</Card>
<Card>
<CardContent className="p-0">
<div className="w-full overflow-auto">
@@ -292,12 +554,12 @@ function RiskListViewContent({
Name
</th>
<th className="h-12 px-4 text-left align-middle font-medium text-tertiary w-1/4">
Probability
Inherent Severity
</th>
<th className="h-12 px-4 text-left align-middle font-medium text-tertiary w-1/4">
Impact
Residual Severity
</th>
<th className="h-12 px-4 text-left align-middle font-medium text-tertiary w-[80px]">
<th className="h-12 px-4 text-left align-middle font-medium text-tertiary w-[120px]">
Action
</th>
</tr>
@@ -331,7 +593,9 @@ function RiskListViewContent({
to={`/organizations/${organizationId}/risks/${risk.id}`}
className="block p-4 h-full w-full"
>
{floatToProbabilityText(risk.probability)}
{floatToPercentage(
risk.inherentLikelihood * risk.inherentImpact
)}
</Link>
</td>
<td className="p-0 align-middle w-1/4 whitespace-nowrap">
@@ -339,19 +603,40 @@ function RiskListViewContent({
to={`/organizations/${organizationId}/risks/${risk.id}`}
className="block p-4 h-full w-full"
>
{floatToImpactText(risk.impact)}
{risk.residualLikelihood && risk.residualImpact
? floatToPercentage(
risk.residualLikelihood * risk.residualImpact
)
: "Not set"}
</Link>
</td>
<td className="p-4 align-middle w-[80px]">
<Button
variant="ghost"
size="icon"
onClick={() => {
setRiskToDelete({ id: risk.id, name: risk.name });
}}
>
<Trash2 className="h-4 w-4 text-danger" />
</Button>
<td className="p-4 align-middle w-[120px]">
<div className="flex">
<Button
variant="ghost"
size="icon"
asChild
className="mr-1"
>
<Link
to={`/organizations/${organizationId}/risks/${risk.id}/edit`}
>
<Edit className="h-4 w-4 text-tertiary" />
</Link>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => {
setRiskToDelete({
id: risk.id,
name: risk.name,
});
}}
>
<Trash2 className="h-4 w-4 text-danger" />
</Button>
</div>
</td>
</tr>
))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,184 @@
/**
* @generated SignedSource<<c0da8faf6e1669e8cf890804a805b2d1>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type EditRiskViewQuery$variables = {
riskId: string;
};
export type EditRiskViewQuery$data = {
readonly risk: {
readonly description?: string;
readonly id?: string;
readonly inherentImpact?: number;
readonly inherentLikelihood?: number;
readonly name?: string;
readonly residualImpact?: number;
readonly residualLikelihood?: number;
};
};
export type EditRiskViewQuery = {
response: EditRiskViewQuery$data;
variables: EditRiskViewQuery$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 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "inherentLikelihood",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "inherentImpact",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualLikelihood",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualImpact",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditRiskViewQuery",
"selections": [
{
"alias": "risk",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/)
],
"type": "Risk",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditRiskViewQuery",
"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*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/)
],
"type": "Risk",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "0cf5e4b47335aeb1fe04520d137942d1",
"id": null,
"metadata": {},
"name": "EditRiskViewQuery",
"operationKind": "query",
"text": "query EditRiskViewQuery(\n $riskId: ID!\n) {\n risk: node(id: $riskId) {\n __typename\n ... on Risk {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "d057f56759426d63136cef80269dc8a3";
export default node;

View File

@@ -0,0 +1,167 @@
/**
* @generated SignedSource<<c9530c22837ce495259b6d3ac404da87>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type UpdateRiskInput = {
description?: string | null | undefined;
id: string;
inherentImpact?: number | null | undefined;
inherentLikelihood?: number | null | undefined;
name?: string | null | undefined;
residualImpact?: number | null | undefined;
residualLikelihood?: number | null | undefined;
};
export type EditRiskViewUpdateRiskMutation$variables = {
input: UpdateRiskInput;
};
export type EditRiskViewUpdateRiskMutation$data = {
readonly updateRisk: {
readonly risk: {
readonly description: string;
readonly id: string;
readonly inherentImpact: number;
readonly inherentLikelihood: number;
readonly name: string;
readonly residualImpact: number;
readonly residualLikelihood: number;
readonly updatedAt: string;
};
};
};
export type EditRiskViewUpdateRiskMutation = {
response: EditRiskViewUpdateRiskMutation$data;
variables: EditRiskViewUpdateRiskMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateRiskPayload",
"kind": "LinkedField",
"name": "updateRisk",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Risk",
"kind": "LinkedField",
"name": "risk",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"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": "inherentLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "inherentImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditRiskViewUpdateRiskMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditRiskViewUpdateRiskMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "1fb9a7f1a9a6dc2fd8b1c3235336324b",
"id": null,
"metadata": {},
"name": "EditRiskViewUpdateRiskMutation",
"operationKind": "mutation",
"text": "mutation EditRiskViewUpdateRiskMutation(\n $input: UpdateRiskInput!\n) {\n updateRisk(input: $input) {\n risk {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n updatedAt\n }\n }\n}\n"
}
};
})();
(node as any).hash = "75f21269b0c81f77ce4e3547010059ee";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<6488a6430dca9def82544e0ac0ca0db8>>
* @generated SignedSource<<63841b5a52846eba02cc6d442cfd4133>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,10 +11,12 @@
import { ConcreteRequest } from 'relay-runtime';
export type CreateRiskInput = {
description: string;
impact: number;
inherentImpact: number;
inherentLikelihood: number;
name: string;
organizationId: string;
probability: number;
residualImpact?: number | null | undefined;
residualLikelihood?: number | null | undefined;
};
export type NewRiskViewCreateRiskMutation$variables = {
connections: ReadonlyArray<string>;
@@ -27,9 +29,11 @@ export type NewRiskViewCreateRiskMutation$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 residualImpact: number;
readonly residualLikelihood: number;
readonly updatedAt: string;
};
};
@@ -99,14 +103,28 @@ v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "probability",
"name": "inherentLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "impact",
"name": "inherentImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualImpact",
"storageKey": null
},
{
@@ -195,16 +213,16 @@ return {
]
},
"params": {
"cacheID": "345bb376e4168b3b964b8a7ffa4ccbb9",
"cacheID": "86d84d72c424807216edf39eb8f0c34f",
"id": null,
"metadata": {},
"name": "NewRiskViewCreateRiskMutation",
"operationKind": "mutation",
"text": "mutation NewRiskViewCreateRiskMutation(\n $input: CreateRiskInput!\n) {\n createRisk(input: $input) {\n riskEdge {\n node {\n id\n name\n description\n probability\n impact\n createdAt\n updatedAt\n }\n }\n }\n}\n"
"text": "mutation NewRiskViewCreateRiskMutation(\n $input: CreateRiskInput!\n) {\n createRisk(input: $input) {\n riskEdge {\n node {\n id\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n createdAt\n updatedAt\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "3cf30aab7c80cc9308343c6ec1577c5a";
(node as any).hash = "7ad6c27c192e419839b8105eb2ca9ae7";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e9f2db8dc7c26cc332a75299317684b7>>
* @generated SignedSource<<ada1e6614842f2accf7bbe92a4e845a4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -190,14 +190,28 @@ return {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "probability",
"name": "inherentLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "impact",
"name": "inherentImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualImpact",
"storageKey": null
},
{
@@ -308,16 +322,16 @@ return {
]
},
"params": {
"cacheID": "33b15ec024ff6f9963d539be971aef7b",
"cacheID": "afcf1c15c2ed7722673da85d53acfef3",
"id": null,
"metadata": {},
"name": "RiskListViewPaginationQuery",
"operationKind": "query",
"text": "query RiskListViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...RiskListView_risks_pbnwq\n id\n }\n}\n\nfragment RiskListView_risks_pbnwq on Organization {\n risks(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n probability\n impact\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
"text": "query RiskListViewPaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...RiskListView_risks_pbnwq\n id\n }\n}\n\nfragment RiskListView_risks_pbnwq on Organization {\n risks(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "59f58f0b1c17242dea5e762b6337bf98";
(node as any).hash = "7da0a3d5e4fb09ce7f6b7ddf233702ab";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<f8cd1d0374c4a6de2b577c7fcb35c984>>
* @generated SignedSource<<5301e57ef0b2621712f4c8ca693afdeb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -192,14 +192,28 @@ return {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "probability",
"name": "inherentLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "impact",
"name": "inherentImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualImpact",
"storageKey": null
},
{
@@ -310,12 +324,12 @@ return {
]
},
"params": {
"cacheID": "78d7871004210dec73a83511b147856f",
"cacheID": "56ddd15a8a41c48c01bf0be667569473",
"id": null,
"metadata": {},
"name": "RiskListViewQuery",
"operationKind": "query",
"text": "query RiskListViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...RiskListView_risks_pbnwq\n }\n}\n\nfragment RiskListView_risks_pbnwq on Organization {\n risks(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n probability\n impact\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
"text": "query RiskListViewQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ...RiskListView_risks_pbnwq\n }\n}\n\nfragment RiskListView_risks_pbnwq on Organization {\n risks(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n id\n}\n"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<1d60fbe24f12a69953d5ade8fb94ec1e>>
* @generated SignedSource<<c2e3450de2b84e18c9ade3ac1bed3b69>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,9 +19,11 @@ export type RiskListView_risks$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 residualImpact: number;
readonly residualLikelihood: number;
readonly updatedAt: string;
};
}>;
@@ -143,14 +145,28 @@ return {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "probability",
"name": "inherentLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "impact",
"name": "inherentImpact",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualLikelihood",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "residualImpact",
"storageKey": null
},
{
@@ -255,6 +271,6 @@ return {
};
})();
(node as any).hash = "59f58f0b1c17242dea5e762b6337bf98";
(node as any).hash = "7da0a3d5e4fb09ce7f6b7ddf233702ab";
export default node;

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<73623cc6511c56dd418345cd64d500d5>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateRiskMitigationMappingInput = {
mitigationId: string;
riskId: string;
};
export type ShowRiskViewCreateRiskMitigationMappingMutation$variables = {
input: CreateRiskMitigationMappingInput;
};
export type ShowRiskViewCreateRiskMitigationMappingMutation$data = {
readonly createRiskMitigationMapping: {
readonly success: boolean;
};
};
export type ShowRiskViewCreateRiskMitigationMappingMutation = {
response: ShowRiskViewCreateRiskMitigationMappingMutation$data;
variables: ShowRiskViewCreateRiskMitigationMappingMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "CreateRiskMitigationMappingPayload",
"kind": "LinkedField",
"name": "createRiskMitigationMapping",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowRiskViewCreateRiskMitigationMappingMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowRiskViewCreateRiskMitigationMappingMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "5a8744dfb9cf5f977c2702d848aab7ae",
"id": null,
"metadata": {},
"name": "ShowRiskViewCreateRiskMitigationMappingMutation",
"operationKind": "mutation",
"text": "mutation ShowRiskViewCreateRiskMitigationMappingMutation(\n $input: CreateRiskMitigationMappingInput!\n) {\n createRiskMitigationMapping(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "bdb664dab4a9e655c0923cfdd4ab0fe9";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<16f4621b72343a04c88df9d0b137c9fd>>
* @generated SignedSource<<0ecdbc802635f485dd265dac34b04386>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,23 +9,21 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateRiskMappingInput = {
impact: number;
mitigationId: string;
probability: number;
export type CreateRiskPolicyMappingInput = {
policyId: string;
riskId: string;
};
export type ShowRiskViewCreateRiskMappingMutation$variables = {
input: CreateRiskMappingInput;
export type ShowRiskViewCreateRiskPolicyMappingMutation$variables = {
input: CreateRiskPolicyMappingInput;
};
export type ShowRiskViewCreateRiskMappingMutation$data = {
readonly createRiskMapping: {
export type ShowRiskViewCreateRiskPolicyMappingMutation$data = {
readonly createRiskPolicyMapping: {
readonly success: boolean;
};
};
export type ShowRiskViewCreateRiskMappingMutation = {
response: ShowRiskViewCreateRiskMappingMutation$data;
variables: ShowRiskViewCreateRiskMappingMutation$variables;
export type ShowRiskViewCreateRiskPolicyMappingMutation = {
response: ShowRiskViewCreateRiskPolicyMappingMutation$data;
variables: ShowRiskViewCreateRiskPolicyMappingMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -46,9 +44,9 @@ v1 = [
"variableName": "input"
}
],
"concreteType": "CreateRiskMappingPayload",
"concreteType": "CreateRiskPolicyMappingPayload",
"kind": "LinkedField",
"name": "createRiskMapping",
"name": "createRiskPolicyMapping",
"plural": false,
"selections": [
{
@@ -67,7 +65,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowRiskViewCreateRiskMappingMutation",
"name": "ShowRiskViewCreateRiskPolicyMappingMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -76,20 +74,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowRiskViewCreateRiskMappingMutation",
"name": "ShowRiskViewCreateRiskPolicyMappingMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "32dfd9049a8c6c1d404528bac1316346",
"cacheID": "7aac9b6766d79dcc4785f72b6f6805a8",
"id": null,
"metadata": {},
"name": "ShowRiskViewCreateRiskMappingMutation",
"name": "ShowRiskViewCreateRiskPolicyMappingMutation",
"operationKind": "mutation",
"text": "mutation ShowRiskViewCreateRiskMappingMutation(\n $input: CreateRiskMappingInput!\n) {\n createRiskMapping(input: $input) {\n success\n }\n}\n"
"text": "mutation ShowRiskViewCreateRiskPolicyMappingMutation(\n $input: CreateRiskPolicyMappingInput!\n) {\n createRiskPolicyMapping(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "02f82b14cc6cf7c2e39dad4613e9d191";
(node as any).hash = "5a10339ed779d863662406775e38ea7b";
export default node;

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<406a706e512ed01c1924632306a4f81c>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteRiskMitigationMappingInput = {
mitigationId: string;
riskId: string;
};
export type ShowRiskViewDeleteRiskMitigationMappingMutation$variables = {
input: DeleteRiskMitigationMappingInput;
};
export type ShowRiskViewDeleteRiskMitigationMappingMutation$data = {
readonly deleteRiskMitigationMapping: {
readonly success: boolean;
};
};
export type ShowRiskViewDeleteRiskMitigationMappingMutation = {
response: ShowRiskViewDeleteRiskMitigationMappingMutation$data;
variables: ShowRiskViewDeleteRiskMitigationMappingMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "DeleteRiskMitigationMappingPayload",
"kind": "LinkedField",
"name": "deleteRiskMitigationMapping",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowRiskViewDeleteRiskMitigationMappingMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowRiskViewDeleteRiskMitigationMappingMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "9a4b56b457c7fb53dc2583da93a3aaa4",
"id": null,
"metadata": {},
"name": "ShowRiskViewDeleteRiskMitigationMappingMutation",
"operationKind": "mutation",
"text": "mutation ShowRiskViewDeleteRiskMitigationMappingMutation(\n $input: DeleteRiskMitigationMappingInput!\n) {\n deleteRiskMitigationMapping(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "d9fece39ecffbd551662d6491088ce4c";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<45a4aca30e16d598cd188f3cbc041a0d>>
* @generated SignedSource<<d11690667058b0e7d271b815f510c44c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,21 +9,21 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteRiskMappingInput = {
mitigationId: string;
export type DeleteRiskPolicyMappingInput = {
policyId: string;
riskId: string;
};
export type ShowRiskViewDeleteRiskMappingMutation$variables = {
input: DeleteRiskMappingInput;
export type ShowRiskViewDeleteRiskPolicyMappingMutation$variables = {
input: DeleteRiskPolicyMappingInput;
};
export type ShowRiskViewDeleteRiskMappingMutation$data = {
readonly deleteRiskMapping: {
export type ShowRiskViewDeleteRiskPolicyMappingMutation$data = {
readonly deleteRiskPolicyMapping: {
readonly success: boolean;
};
};
export type ShowRiskViewDeleteRiskMappingMutation = {
response: ShowRiskViewDeleteRiskMappingMutation$data;
variables: ShowRiskViewDeleteRiskMappingMutation$variables;
export type ShowRiskViewDeleteRiskPolicyMappingMutation = {
response: ShowRiskViewDeleteRiskPolicyMappingMutation$data;
variables: ShowRiskViewDeleteRiskPolicyMappingMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -44,9 +44,9 @@ v1 = [
"variableName": "input"
}
],
"concreteType": "DeleteRiskMappingPayload",
"concreteType": "DeleteRiskPolicyMappingPayload",
"kind": "LinkedField",
"name": "deleteRiskMapping",
"name": "deleteRiskPolicyMapping",
"plural": false,
"selections": [
{
@@ -65,7 +65,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "ShowRiskViewDeleteRiskMappingMutation",
"name": "ShowRiskViewDeleteRiskPolicyMappingMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -74,20 +74,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowRiskViewDeleteRiskMappingMutation",
"name": "ShowRiskViewDeleteRiskPolicyMappingMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "847faec465a30f457c57c5c1524a707d",
"cacheID": "a3e208ccc9b5c1248450edaec7ef8417",
"id": null,
"metadata": {},
"name": "ShowRiskViewDeleteRiskMappingMutation",
"name": "ShowRiskViewDeleteRiskPolicyMappingMutation",
"operationKind": "mutation",
"text": "mutation ShowRiskViewDeleteRiskMappingMutation(\n $input: DeleteRiskMappingInput!\n) {\n deleteRiskMapping(input: $input) {\n success\n }\n}\n"
"text": "mutation ShowRiskViewDeleteRiskPolicyMappingMutation(\n $input: DeleteRiskPolicyMappingInput!\n) {\n deleteRiskPolicyMapping(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "41aa62a4185b1e6061bd7e06abbec85d";
(node as any).hash = "5e6b5677587f3c8bad3cec9d751952e1";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<771e7ea5d1af4e722dc5790db8abd623>>
* @generated SignedSource<<da6cda7380aec978f37e3adc35942e03>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type ShowRiskViewOrganizationMitigationsQuery$variables = {
organizationId: string;
@@ -23,7 +22,6 @@ export type ShowRiskViewOrganizationMitigationsQuery$data = {
readonly category: string;
readonly description: string;
readonly id: string;
readonly importance: MitigationImportance;
readonly name: string;
readonly state: MitigationState;
};
@@ -104,13 +102,6 @@ v4 = [
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "importance",
"storageKey": null
},
{
"alias": null,
"args": null,
@@ -253,7 +244,7 @@ return {
]
},
"params": {
"cacheID": "ac4f3fc31facab3cb2b9455751a4a528",
"cacheID": "a73345064022bd2f11ef2f971cd898b5",
"id": null,
"metadata": {
"connection": [
@@ -270,11 +261,11 @@ return {
},
"name": "ShowRiskViewOrganizationMitigationsQuery",
"operationKind": "query",
"text": "query ShowRiskViewOrganizationMitigationsQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ShowRiskViewOrganizationMitigationsQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "608bcf7b0e53529673b25ac7762627e2";
(node as any).hash = "76ce05cd9f5a367595eb31b5df24a679";
export default node;

View File

@@ -0,0 +1,255 @@
/**
* @generated SignedSource<<86be887d4ec59050fd69900fd80863a2>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ShowRiskViewOrganizationPoliciesQuery$variables = {
organizationId: string;
};
export type ShowRiskViewOrganizationPoliciesQuery$data = {
readonly organization: {
readonly id: string;
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly name: string;
readonly status: PolicyStatus;
};
}>;
};
};
};
export type ShowRiskViewOrganizationPoliciesQuery = {
response: ShowRiskViewOrganizationPoliciesQuery$data;
variables: ShowRiskViewOrganizationPoliciesQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
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": "PolicyEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Policy",
"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": "status",
"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": "ShowRiskViewOrganizationPoliciesQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": "policies",
"args": null,
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "__Organization__policies_connection",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": null
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "ShowRiskViewOrganizationPoliciesQuery",
"selections": [
{
"alias": "organization",
"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": "PolicyConnection",
"kind": "LinkedField",
"name": "policies",
"plural": false,
"selections": (v4/*: any*/),
"storageKey": "policies(first:100)"
},
{
"alias": null,
"args": (v5/*: any*/),
"filters": null,
"handle": "connection",
"key": "Organization__policies",
"kind": "LinkedHandle",
"name": "policies"
}
],
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "a229f1f805e79f8de378305e021d7fea",
"id": null,
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"organization",
"policies"
]
}
]
},
"name": "ShowRiskViewOrganizationPoliciesQuery",
"operationKind": "query",
"text": "query ShowRiskViewOrganizationPoliciesQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n id\n ... on Organization {\n policies(first: 100) {\n edges {\n node {\n id\n name\n status\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "26cbf19cd716aee7413931357b9c5354";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<61ecdce52780dbfd62783deecb27645d>>
* @generated SignedSource<<a6b1be8aa499bb8709a4d4e04edb01b7>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,17 +9,29 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type MitigationImportance = "ADVANCED" | "MANDATORY" | "PREFERRED";
export type MitigationState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type PolicyStatus = "ACTIVE" | "DRAFT";
export type ShowRiskViewQuery$variables = {
riskId: string;
};
export type ShowRiskViewQuery$data = {
readonly node: {
readonly controls?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: string;
readonly description: string;
readonly id: string;
readonly name: string;
readonly referenceId: string;
};
}>;
};
readonly createdAt?: string;
readonly description?: string;
readonly id: string;
readonly impact?: number;
readonly inherentImpact?: number;
readonly inherentLikelihood?: number;
readonly mitigations?: {
readonly edges: ReadonlyArray<{
readonly node: {
@@ -27,14 +39,24 @@ export type ShowRiskViewQuery$data = {
readonly createdAt: string;
readonly description: string;
readonly id: string;
readonly importance: MitigationImportance;
readonly name: string;
readonly state: MitigationState;
};
}>;
};
readonly name?: string;
readonly probability?: number;
readonly policies?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly createdAt: string;
readonly id: string;
readonly name: string;
readonly status: PolicyStatus;
};
}>;
};
readonly residualImpact?: number;
readonly residualLikelihood?: number;
readonly updatedAt?: string;
};
};
@@ -83,38 +105,84 @@ v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "probability",
"name": "inherentLikelihood",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "impact",
"name": "inherentImpact",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"name": "residualLikelihood",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"name": "residualImpact",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v10 = [
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
},
v13 = {
"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
},
v14 = [
{
"alias": null,
"args": null,
@@ -141,14 +209,7 @@ v10 = [
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "importance",
"storageKey": null
},
(v7/*: any*/),
(v9/*: any*/),
{
"alias": null,
"args": null,
@@ -156,47 +217,92 @@ v10 = [
"name": "state",
"storageKey": null
},
(v9/*: any*/)
(v11/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
(v12/*: any*/)
],
"storageKey": null
},
(v13/*: any*/)
],
v15 = [
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"concreteType": "PolicyEdge",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"concreteType": "Policy",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "status",
"storageKey": null
},
(v9/*: any*/),
(v11/*: any*/)
],
"storageKey": null
},
(v12/*: any*/)
],
"storageKey": null
},
(v13/*: any*/)
],
v16 = [
{
"alias": null,
"args": null,
"concreteType": "ControlEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"concreteType": "Control",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "referenceId",
"storageKey": null
},
(v3/*: any*/),
(v4/*: any*/),
(v9/*: any*/),
(v11/*: any*/)
],
"storageKey": null
}
},
(v12/*: any*/)
],
"storageKey": null
}
},
(v13/*: any*/)
],
v11 = [
v17 = [
{
"kind": "Literal",
"name": "first",
@@ -228,6 +334,8 @@ return {
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"alias": "mitigations",
"args": null,
@@ -235,7 +343,27 @@ return {
"kind": "LinkedField",
"name": "__Risk__mitigations_connection",
"plural": false,
"selections": (v10/*: any*/),
"selections": (v14/*: any*/),
"storageKey": null
},
{
"alias": "policies",
"args": null,
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "__Risk__policies_connection",
"plural": false,
"selections": (v15/*: any*/),
"storageKey": null
},
{
"alias": "controls",
"args": null,
"concreteType": "ControlConnection",
"kind": "LinkedField",
"name": "__Risk__controls_connection",
"plural": false,
"selections": (v16/*: any*/),
"storageKey": null
}
],
@@ -263,7 +391,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v9/*: any*/),
(v11/*: any*/),
(v2/*: any*/),
{
"kind": "InlineFragment",
@@ -274,24 +402,64 @@ return {
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
{
"alias": null,
"args": (v11/*: any*/),
"args": (v17/*: any*/),
"concreteType": "MitigationConnection",
"kind": "LinkedField",
"name": "mitigations",
"plural": false,
"selections": (v10/*: any*/),
"selections": (v14/*: any*/),
"storageKey": "mitigations(first:100)"
},
{
"alias": null,
"args": (v11/*: any*/),
"args": (v17/*: any*/),
"filters": null,
"handle": "connection",
"key": "Risk__mitigations",
"kind": "LinkedHandle",
"name": "mitigations"
},
{
"alias": null,
"args": (v17/*: any*/),
"concreteType": "PolicyConnection",
"kind": "LinkedField",
"name": "policies",
"plural": false,
"selections": (v15/*: any*/),
"storageKey": "policies(first:100)"
},
{
"alias": null,
"args": (v17/*: any*/),
"filters": null,
"handle": "connection",
"key": "Risk__policies",
"kind": "LinkedHandle",
"name": "policies"
},
{
"alias": null,
"args": (v17/*: any*/),
"concreteType": "ControlConnection",
"kind": "LinkedField",
"name": "controls",
"plural": false,
"selections": (v16/*: any*/),
"storageKey": "controls(first:100)"
},
{
"alias": null,
"args": (v17/*: any*/),
"filters": null,
"handle": "connection",
"key": "Risk__controls",
"kind": "LinkedHandle",
"name": "controls"
}
],
"type": "Risk",
@@ -303,7 +471,7 @@ return {
]
},
"params": {
"cacheID": "b30cf22fe56e7cb776d6d4a5af81e358",
"cacheID": "4463fac13f91564f513bfdabcc3e0063",
"id": null,
"metadata": {
"connection": [
@@ -315,16 +483,34 @@ return {
"node",
"mitigations"
]
},
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"node",
"policies"
]
},
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"node",
"controls"
]
}
]
},
"name": "ShowRiskViewQuery",
"operationKind": "query",
"text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n probability\n impact\n createdAt\n updatedAt\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n importance\n createdAt\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
"text": "query ShowRiskViewQuery(\n $riskId: ID!\n) {\n node(id: $riskId) {\n __typename\n id\n ... on Risk {\n name\n description\n inherentLikelihood\n inherentImpact\n residualLikelihood\n residualImpact\n createdAt\n updatedAt\n mitigations(first: 100) {\n edges {\n node {\n id\n name\n description\n category\n createdAt\n state\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n policies(first: 100) {\n edges {\n node {\n id\n name\n status\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n controls(first: 100) {\n edges {\n node {\n id\n referenceId\n name\n description\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "77f36d2a4405051ede742be63d3e1e38";
(node as any).hash = "68db9a4877abc338711844de6da38cb7";
export default node;

188
package-lock.json generated
View File

@@ -33,6 +33,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",
@@ -3539,6 +3540,193 @@
}
}
},
"node_modules/@radix-ui/react-switch": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.4.tgz",
"integrity": "sha512-zGP6W8plLeogoeGMiTHJ/uvf+TE1C2chVsEwfP8YlvpQKJHktG+iCkUtCLGPAuDV8/qDSmIRPm4NggaTxFMVBQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-controllable-state": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",

View File

@@ -175,6 +175,72 @@ WHERE %s
return nil
}
func (c *Controls) LoadByRiskID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
riskID gid.GID,
cursor *page.Cursor[ControlOrderField],
) error {
q := `
WITH ctrl AS (
SELECT DISTINCT
c.id,
c.reference_id,
c.framework_id,
c.tenant_id,
c.name,
c.description,
c.created_at,
c.updated_at
FROM
controls c
LEFT JOIN
controls_policies cp ON c.id = cp.control_id
LEFT JOIN
risks_policies rp ON cp.policy_id = rp.policy_id
LEFT JOIN
controls_mitigations cm ON c.id = cm.control_id
LEFT JOIN
risks_mitigations rm ON (rm.mitigation_id = cm.mitigation_id)
WHERE
rp.risk_id = @risk_id OR rm.risk_id = @risk_id
)
SELECT
id,
reference_id,
framework_id,
tenant_id,
name,
description,
created_at,
updated_at
FROM
ctrl
WHERE %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.NamedArgs{"risk_id": riskID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query controls: %w", err)
}
controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control])
if err != nil {
return fmt.Errorf("cannot collect controls: %w", err)
}
*c = controls
return nil
}
func (c *Controls) LoadByFrameworkID(
ctx context.Context,
conn pg.Conn,

View File

@@ -0,0 +1,7 @@
ALTER TABLE risks RENAME COLUMN probability TO likelihood;
ALTER TABLE risks ADD COLUMN residual_likelihood float;
ALTER TABLE risks ADD COLUMN residual_impact float;
ALTER TABLE risks ALTER COLUMN likelihood TYPE float;
ALTER TABLE risks ALTER COLUMN impact TYPE float;
ALTER TABLE risks_mitigations DROP COLUMN probability, DROP COLUMN impact;

View File

@@ -0,0 +1,5 @@
UPDATE risks SET residual_impact = impact WHERE residual_impact IS NULL;
UPDATE risks SET residual_likelihood = likelihood WHERE residual_likelihood IS NULL;
ALTER TABLE risks ALTER COLUMN residual_impact SET NOT NULL;
ALTER TABLE risks ALTER COLUMN residual_likelihood SET NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE risks RENAME COLUMN likelihood TO inherent_likelihood;
ALTER TABLE risks RENAME COLUMN impact TO inherent_impact;

View File

@@ -0,0 +1,7 @@
CREATE TABLE risks_policies (
risk_id TEXT NOT NULL REFERENCES risks(id),
policy_id TEXT NOT NULL REFERENCES policies(id),
tenant_id TEXT NOT NULL,
created_at timestamp NOT NULL,
PRIMARY KEY (risk_id, policy_id)
);

View File

@@ -299,3 +299,66 @@ WHERE %s
return nil
}
func (p *Policies) LoadByRiskID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
riskID gid.GID,
cursor *page.Cursor[PolicyOrderField],
) error {
q := `
WITH plcs AS (
SELECT
p.id,
p.tenant_id,
p.organization_id,
p.owner_id,
p.name,
p.content,
p.status,
p.review_date,
p.created_at,
p.updated_at
FROM
policies p
INNER JOIN
risks_policies rp ON p.id = rp.policy_id
WHERE
rp.risk_id = @risk_id
)
SELECT
id,
organization_id,
owner_id,
name,
content,
status,
review_date,
created_at,
updated_at
FROM
plcs
WHERE %s
AND %s
`
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
args := pgx.StrictNamedArgs{"risk_id": riskID}
maps.Copy(args, scope.SQLArguments())
maps.Copy(args, cursor.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot query policies: %w", err)
}
policies, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Policy])
if err != nil {
return fmt.Errorf("cannot collect policies: %w", err)
}
*p = policies
return nil
}

View File

@@ -28,14 +28,16 @@ import (
type (
Risk struct {
ID gid.GID
OrganizationID gid.GID
Name string
Description string
Probability float64
Impact float64
CreatedAt time.Time
UpdatedAt time.Time
ID gid.GID `db:"id"`
OrganizationID gid.GID `db:"organization_id"`
Name string `db:"name"`
Description string `db:"description"`
InherentLikelihood float64 `db:"inherent_likelihood"`
InherentImpact float64 `db:"inherent_impact"`
ResidualLikelihood float64 `db:"residual_likelihood"`
ResidualImpact float64 `db:"residual_impact"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
Risks []*Risk
@@ -50,6 +52,14 @@ func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey {
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (r *Risk) InherentSeverity() float64 {
return r.InherentLikelihood * r.InherentImpact
}
func (r *Risk) ResidualSeverity() float64 {
return r.ResidualLikelihood * r.ResidualImpact
}
func (r *Risks) LoadByMitigationID(
ctx context.Context,
conn pg.Conn,
@@ -65,8 +75,10 @@ WITH rsks AS (
r.tenant_id,
r.name,
r.description,
r.probability,
r.impact,
r.inherent_likelihood,
r.inherent_impact,
r.residual_likelihood,
r.residual_impact,
r.created_at,
r.updated_at
FROM
@@ -81,8 +93,10 @@ SELECT
organization_id,
name,
description,
probability,
impact,
inherent_likelihood,
inherent_impact,
residual_likelihood,
residual_impact,
created_at,
updated_at
FROM
@@ -123,8 +137,10 @@ SELECT
organization_id,
name,
description,
probability,
impact,
inherent_likelihood,
inherent_impact,
residual_likelihood,
residual_impact,
created_at,
updated_at
FROM risks
@@ -164,8 +180,10 @@ SELECT
organization_id,
name,
description,
probability,
impact,
inherent_likelihood,
inherent_impact,
residual_likelihood,
residual_impact,
created_at,
updated_at
FROM risks
@@ -199,20 +217,22 @@ func (r *Risk) Insert(
scope Scoper,
) error {
q := `
INSERT INTO risks (id, tenant_id, organization_id, name, description, probability, impact, created_at, updated_at)
VALUES (@id, @tenant_id, @organization_id, @name, @description, @probability, @impact, @created_at, @updated_at)
INSERT INTO risks (id, tenant_id, organization_id, name, description, inherent_likelihood, inherent_impact, residual_likelihood, residual_impact, created_at, updated_at)
VALUES (@id, @tenant_id, @organization_id, @name, @description, @inherent_likelihood, @inherent_impact, @residual_likelihood, @residual_impact, @created_at, @updated_at)
`
args := pgx.StrictNamedArgs{
"id": r.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": r.OrganizationID,
"name": r.Name,
"description": r.Description,
"probability": r.Probability,
"impact": r.Impact,
"created_at": r.CreatedAt,
"updated_at": r.UpdatedAt,
"id": r.ID,
"tenant_id": scope.GetTenantID(),
"organization_id": r.OrganizationID,
"name": r.Name,
"description": r.Description,
"inherent_likelihood": r.InherentLikelihood,
"inherent_impact": r.InherentImpact,
"residual_likelihood": r.ResidualLikelihood,
"residual_impact": r.ResidualImpact,
"created_at": r.CreatedAt,
"updated_at": r.UpdatedAt,
}
_, err := conn.Exec(ctx, q, args)
@@ -229,20 +249,25 @@ UPDATE risks
SET
name = @name,
description = @description,
probability = @probability,
impact = @impact,
inherent_likelihood = @inherent_likelihood,
inherent_impact = @inherent_impact,
residual_likelihood = @residual_likelihood,
residual_impact = @residual_impact,
updated_at = @updated_at
WHERE %s
AND tenant_id = @tenant_id
AND id = @risk_id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"name": r.Name,
"description": r.Description,
"probability": r.Probability,
"impact": r.Impact,
"updated_at": r.UpdatedAt,
"risk_id": r.ID,
"name": r.Name,
"description": r.Description,
"inherent_likelihood": r.InherentLikelihood,
"inherent_impact": r.InherentImpact,
"residual_likelihood": r.ResidualLikelihood,
"residual_impact": r.ResidualImpact,
"updated_at": r.UpdatedAt,
}
maps.Copy(args, scope.SQLArguments())

View File

@@ -31,8 +31,6 @@ type (
MitigationID gid.GID `db:"mitigation_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
Probability float64 `db:"probability"`
Impact float64 `db:"impact"`
}
RiskMitigations []*RiskMitigation
@@ -49,16 +47,12 @@ INSERT INTO
risk_id,
mitigation_id,
tenant_id,
probability,
impact,
created_at
)
VALUES (
@risk_id,
@mitigation_id,
@tenant_id,
@probability,
@impact,
@created_at
);
`
@@ -67,8 +61,6 @@ VALUES (
"risk_id": rm.RiskID,
"mitigation_id": rm.MitigationID,
"tenant_id": scope.GetTenantID(),
"probability": rm.Probability,
"impact": rm.Impact,
"created_at": rm.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)

View File

@@ -0,0 +1,95 @@
// 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 (
"context"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
RiskPolicy struct {
RiskID gid.GID `db:"risk_id"`
PolicyID gid.GID `db:"policy_id"`
TenantID gid.TenantID `db:"tenant_id"`
CreatedAt time.Time `db:"created_at"`
}
RiskPolicies []*RiskPolicy
)
func (rp RiskPolicy) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO
risks_policies (
risk_id,
policy_id,
tenant_id,
created_at
)
VALUES (
@risk_id,
@policy_id,
@tenant_id,
@created_at
);
`
args := pgx.StrictNamedArgs{
"risk_id": rp.RiskID,
"policy_id": rp.PolicyID,
"tenant_id": scope.GetTenantID(),
"created_at": rp.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (rp RiskPolicy) Delete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
DELETE
FROM
risks_policies
WHERE
%s
AND risk_id = @risk_id
AND policy_id = @policy_id;
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"risk_id": rp.RiskID,
"policy_id": rp.PolicyID,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}

View File

@@ -177,7 +177,6 @@ func (s ControlService) DeletePolicyMapping(
)
}
// Create creates a new control
func (s ControlService) Create(
ctx context.Context,
req CreateControlRequest,
@@ -208,7 +207,6 @@ func (s ControlService) Create(
return control, nil
}
// Get retrieves a control by ID
func (s ControlService) Get(
ctx context.Context,
controlID gid.GID,
@@ -229,7 +227,6 @@ func (s ControlService) Get(
return control, nil
}
// Update updates an existing control
func (s ControlService) Update(
ctx context.Context,
req UpdateControlRequest,
@@ -254,7 +251,6 @@ func (s ControlService) Update(
return control, nil
}
// Delete removes a control
func (s ControlService) Delete(
ctx context.Context,
controlID gid.GID,
@@ -269,7 +265,6 @@ func (s ControlService) Delete(
)
}
// ListForFrameworkID retrieves all controls for a framework
func (s ControlService) ListForFrameworkID(
ctx context.Context,
frameworkID gid.GID,
@@ -297,41 +292,23 @@ func (s ControlService) ListForFrameworkID(
return page.NewPage(controls, cursor), nil
}
func (s ControlService) ConnectToMitigation(
func (s ControlService) ListForRiskID(
ctx context.Context,
req ConnectControlToMitigationRequest,
) error {
now := time.Now()
riskID gid.GID,
cursor *page.Cursor[coredata.ControlOrderField],
) (*page.Page[*coredata.Control, coredata.ControlOrderField], error) {
var controls coredata.Controls
controlMitigation := &coredata.ControlMitigation{
ControlID: req.ControlID,
MitigationID: req.MitigationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: now,
}
return s.svc.pg.WithConn(
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controlMitigation.Insert(ctx, conn, s.svc.scope)
return controls.LoadByRiskID(ctx, conn, s.svc.scope, riskID, cursor)
},
)
}
// DisconnectFromMitigation removes the link between a control and a mitigation
func (s ControlService) DisconnectFromMitigation(
ctx context.Context,
req DisconnectControlFromMitigationRequest,
) error {
controlMitigation := &coredata.ControlMitigation{
ControlID: req.ControlID,
MitigationID: req.MitigationID,
if err != nil {
return nil, fmt.Errorf("cannot list controls: %w", err)
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return controlMitigation.Delete(ctx, conn, s.svc.scope)
},
)
return page.NewPage(controls, cursor), nil
}

View File

@@ -209,3 +209,24 @@ func (s *PolicyService) ListForControlID(
return page.NewPage(policies, cursor), nil
}
func (s *PolicyService) ListForRiskID(
ctx context.Context,
riskID gid.GID,
cursor *page.Cursor[coredata.PolicyOrderField],
) (*page.Page[*coredata.Policy, coredata.PolicyOrderField], error) {
var policies coredata.Policies
err := s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return policies.LoadByRiskID(ctx, conn, s.svc.scope, riskID, cursor)
},
)
if err != nil {
return nil, err
}
return page.NewPage(policies, cursor), nil
}

View File

@@ -31,19 +31,23 @@ type (
}
CreateRiskRequest struct {
OrganizationID gid.GID
Name string
Description string
Probability float64
Impact float64
OrganizationID gid.GID
Name string
Description string
InherentLikelihood float64
InherentImpact float64
ResidualLikelihood *float64
ResidualImpact *float64
}
UpdateRiskRequest struct {
ID gid.GID
Name *string
Description *string
Probability *float64
Impact *float64
ID gid.GID
Name *string
Description *string
InherentLikelihood *float64
InherentImpact *float64
ResidualLikelihood *float64
ResidualImpact *float64
}
)
@@ -68,20 +72,56 @@ func (s RiskService) ListForMitigationID(
return page.NewPage(risks, cursor), nil
}
func (s RiskService) CreateMapping(
func (s RiskService) CreatePolicyMapping(
ctx context.Context,
riskID gid.GID,
policyID gid.GID,
) error {
riskPolicy := &coredata.RiskPolicy{
RiskID: riskID,
PolicyID: policyID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return riskPolicy.Insert(ctx, conn, s.svc.scope)
},
)
}
func (s RiskService) DeletePolicyMapping(
ctx context.Context,
riskID gid.GID,
policyID gid.GID,
) error {
riskPolicy := &coredata.RiskPolicy{
RiskID: riskID,
PolicyID: policyID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return riskPolicy.Delete(ctx, conn, s.svc.scope)
},
)
}
func (s RiskService) CreateMitigationMapping(
ctx context.Context,
riskID gid.GID,
mitigationID gid.GID,
probability float64,
impact float64,
) error {
riskMitigation := &coredata.RiskMitigation{
RiskID: riskID,
MitigationID: mitigationID,
TenantID: s.svc.scope.GetTenantID(),
CreatedAt: time.Now(),
Probability: probability,
Impact: impact,
}
return s.svc.pg.WithConn(
@@ -92,7 +132,7 @@ func (s RiskService) CreateMapping(
)
}
func (s RiskService) DeleteMapping(
func (s RiskService) DeleteMitigationMapping(
ctx context.Context,
riskID gid.GID,
mitigationID gid.GID,
@@ -123,14 +163,24 @@ func (s RiskService) Create(
}
risk := &coredata.Risk{
ID: riskID,
OrganizationID: req.OrganizationID,
Name: req.Name,
Description: req.Description,
Probability: req.Probability,
Impact: req.Impact,
CreatedAt: now,
UpdatedAt: now,
ID: riskID,
OrganizationID: req.OrganizationID,
Name: req.Name,
Description: req.Description,
InherentLikelihood: req.InherentLikelihood,
InherentImpact: req.InherentImpact,
ResidualLikelihood: req.InherentLikelihood,
ResidualImpact: req.InherentImpact,
CreatedAt: now,
UpdatedAt: now,
}
if req.ResidualLikelihood != nil {
risk.ResidualLikelihood = *req.ResidualLikelihood
}
if req.ResidualImpact != nil {
risk.ResidualImpact = *req.ResidualImpact
}
err = s.svc.pg.WithConn(
@@ -188,12 +238,20 @@ func (s RiskService) Update(
risk.Description = *req.Description
}
if req.Probability != nil {
risk.Probability = *req.Probability
if req.InherentLikelihood != nil {
risk.InherentLikelihood = *req.InherentLikelihood
}
if req.Impact != nil {
risk.Impact = *req.Impact
if req.InherentImpact != nil {
risk.InherentImpact = *req.InherentImpact
}
if req.ResidualLikelihood != nil {
risk.ResidualLikelihood = *req.ResidualLikelihood
}
if req.ResidualImpact != nil {
risk.ResidualImpact = *req.ResidualImpact
}
risk.UpdatedAt = time.Now()

View File

@@ -608,8 +608,12 @@ type Risk implements Node {
id: ID!
name: String!
description: String!
probability: Float!
impact: Float!
inherentLikelihood: Float!
inherentImpact: Float!
inherentSeverity: Float!
residualLikelihood: Float!
residualImpact: Float!
residualSeverity: Float!
mitigations(
first: Int
@@ -619,6 +623,22 @@ type Risk implements Node {
orderBy: MitigationOrder
): MitigationConnection! @goField(forceResolver: true)
policies(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: PolicyOrder
): PolicyConnection! @goField(forceResolver: true)
controls(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: ControlOrder
): ControlConnection! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -831,8 +851,19 @@ type Mutation {
createRisk(input: CreateRiskInput!): CreateRiskPayload!
updateRisk(input: UpdateRiskInput!): UpdateRiskPayload!
deleteRisk(input: DeleteRiskInput!): DeleteRiskPayload!
createRiskMapping(input: CreateRiskMappingInput!): CreateRiskMappingPayload!
deleteRiskMapping(input: DeleteRiskMappingInput!): DeleteRiskMappingPayload!
createRiskMitigationMapping(
input: CreateRiskMitigationMappingInput!
): CreateRiskMitigationMappingPayload!
deleteRiskMitigationMapping(
input: DeleteRiskMitigationMappingInput!
): DeleteRiskMitigationMappingPayload!
createRiskPolicyMapping(
input: CreateRiskPolicyMappingInput!
): CreateRiskPolicyMappingPayload!
deleteRiskPolicyMapping(
input: DeleteRiskPolicyMappingInput!
): DeleteRiskPolicyMappingPayload!
# Evidence mutations
requestEvidence(input: RequestEvidenceInput!): RequestEvidencePayload!
@@ -1033,34 +1064,46 @@ input CreateRiskInput {
organizationId: ID!
name: String!
description: String!
probability: Float!
impact: Float!
inherentLikelihood: Float!
inherentImpact: Float!
residualLikelihood: Float
residualImpact: Float
}
input UpdateRiskInput {
id: ID!
name: String
description: String
probability: Float
impact: Float
inherentLikelihood: Float
inherentImpact: Float
residualLikelihood: Float
residualImpact: Float
}
input DeleteRiskInput {
riskId: ID!
}
input CreateRiskMappingInput {
input CreateRiskMitigationMappingInput {
riskId: ID!
mitigationId: ID!
probability: Float!
impact: Float!
}
input DeleteRiskMappingInput {
input DeleteRiskMitigationMappingInput {
riskId: ID!
mitigationId: ID!
}
input CreateRiskPolicyMappingInput {
riskId: ID!
policyId: ID!
}
input DeleteRiskPolicyMappingInput {
riskId: ID!
policyId: ID!
}
input RequestEvidenceInput {
taskId: ID!
name: String!
@@ -1250,11 +1293,19 @@ type DeleteRiskPayload {
deletedRiskId: ID!
}
type CreateRiskMappingPayload {
type CreateRiskMitigationMappingPayload {
success: Boolean!
}
type DeleteRiskMappingPayload {
type DeleteRiskMitigationMappingPayload {
success: Boolean!
}
type CreateRiskPolicyMappingPayload {
success: Boolean!
}
type DeleteRiskPolicyMappingPayload {
success: Boolean!
}

File diff suppressed because it is too large Load Diff

View File

@@ -45,12 +45,16 @@ func NewRiskEdge(r *coredata.Risk, orderBy coredata.RiskOrderField) *RiskEdge {
func NewRisk(r *coredata.Risk) *Risk {
return &Risk{
ID: r.ID,
Name: r.Name,
Description: r.Description,
Probability: r.Probability,
Impact: r.Impact,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
ID: r.ID,
Name: r.Name,
Description: r.Description,
InherentLikelihood: r.InherentLikelihood,
InherentImpact: r.InherentImpact,
InherentSeverity: r.InherentSeverity(),
ResidualLikelihood: r.ResidualLikelihood,
ResidualImpact: r.ResidualImpact,
ResidualSeverity: r.ResidualSeverity(),
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
}

View File

@@ -147,21 +147,21 @@ type CreatePolicyPayload struct {
}
type CreateRiskInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
Description string `json:"description"`
Probability float64 `json:"probability"`
Impact float64 `json:"impact"`
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
Description string `json:"description"`
InherentLikelihood float64 `json:"inherentLikelihood"`
InherentImpact float64 `json:"inherentImpact"`
ResidualLikelihood *float64 `json:"residualLikelihood,omitempty"`
ResidualImpact *float64 `json:"residualImpact,omitempty"`
}
type CreateRiskMappingInput struct {
type CreateRiskMitigationMappingInput struct {
RiskID gid.GID `json:"riskId"`
MitigationID gid.GID `json:"mitigationId"`
Probability float64 `json:"probability"`
Impact float64 `json:"impact"`
}
type CreateRiskMappingPayload struct {
type CreateRiskMitigationMappingPayload struct {
Success bool `json:"success"`
}
@@ -169,6 +169,15 @@ type CreateRiskPayload struct {
RiskEdge *RiskEdge `json:"riskEdge"`
}
type CreateRiskPolicyMappingInput struct {
RiskID gid.GID `json:"riskId"`
PolicyID gid.GID `json:"policyId"`
}
type CreateRiskPolicyMappingPayload struct {
Success bool `json:"success"`
}
type CreateTaskInput struct {
MitigationID gid.GID `json:"mitigationId"`
Name string `json:"name"`
@@ -269,12 +278,12 @@ type DeleteRiskInput struct {
RiskID gid.GID `json:"riskId"`
}
type DeleteRiskMappingInput struct {
type DeleteRiskMitigationMappingInput struct {
RiskID gid.GID `json:"riskId"`
MitigationID gid.GID `json:"mitigationId"`
}
type DeleteRiskMappingPayload struct {
type DeleteRiskMitigationMappingPayload struct {
Success bool `json:"success"`
}
@@ -282,6 +291,15 @@ type DeleteRiskPayload struct {
DeletedRiskID gid.GID `json:"deletedRiskId"`
}
type DeleteRiskPolicyMappingInput struct {
RiskID gid.GID `json:"riskId"`
PolicyID gid.GID `json:"policyId"`
}
type DeleteRiskPolicyMappingPayload struct {
Success bool `json:"success"`
}
type DeleteTaskInput struct {
TaskID gid.GID `json:"taskId"`
}
@@ -536,14 +554,20 @@ type RequestEvidencePayload struct {
}
type Risk struct {
ID gid.GID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Probability float64 `json:"probability"`
Impact float64 `json:"impact"`
Mitigations *MitigationConnection `json:"mitigations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
InherentLikelihood float64 `json:"inherentLikelihood"`
InherentImpact float64 `json:"inherentImpact"`
InherentSeverity float64 `json:"inherentSeverity"`
ResidualLikelihood float64 `json:"residualLikelihood"`
ResidualImpact float64 `json:"residualImpact"`
ResidualSeverity float64 `json:"residualSeverity"`
Mitigations *MitigationConnection `json:"mitigations"`
Policies *PolicyConnection `json:"policies"`
Controls *ControlConnection `json:"controls"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (Risk) IsNode() {}
@@ -656,11 +680,13 @@ type UpdatePolicyPayload struct {
}
type UpdateRiskInput struct {
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Probability *float64 `json:"probability,omitempty"`
Impact *float64 `json:"impact,omitempty"`
ID gid.GID `json:"id"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
InherentLikelihood *float64 `json:"inherentLikelihood,omitempty"`
InherentImpact *float64 `json:"inherentImpact,omitempty"`
ResidualLikelihood *float64 `json:"residualLikelihood,omitempty"`
ResidualImpact *float64 `json:"residualImpact,omitempty"`
}
type UpdateRiskPayload struct {

View File

@@ -723,11 +723,13 @@ func (r *mutationResolver) CreateRisk(ctx context.Context, input types.CreateRis
risk, err := svc.Risks.Create(
ctx,
probo.CreateRiskRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: input.Description,
Probability: input.Probability,
Impact: input.Impact,
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: input.Description,
InherentLikelihood: input.InherentLikelihood,
InherentImpact: input.InherentImpact,
ResidualLikelihood: input.ResidualLikelihood,
ResidualImpact: input.ResidualImpact,
},
)
if err != nil {
@@ -746,11 +748,13 @@ func (r *mutationResolver) UpdateRisk(ctx context.Context, input types.UpdateRis
risk, err := svc.Risks.Update(
ctx,
probo.UpdateRiskRequest{
ID: input.ID,
Name: input.Name,
Description: input.Description,
Probability: input.Probability,
Impact: input.Impact,
ID: input.ID,
Name: input.Name,
Description: input.Description,
InherentLikelihood: input.InherentLikelihood,
InherentImpact: input.InherentImpact,
ResidualLikelihood: input.ResidualLikelihood,
ResidualImpact: input.ResidualImpact,
},
)
if err != nil {
@@ -776,30 +780,58 @@ func (r *mutationResolver) DeleteRisk(ctx context.Context, input types.DeleteRis
}, nil
}
// CreateRiskMapping is the resolver for the createRiskMapping field.
func (r *mutationResolver) CreateRiskMapping(ctx context.Context, input types.CreateRiskMappingInput) (*types.CreateRiskMappingPayload, error) {
// CreateRiskMitigationMapping is the resolver for the createRiskMitigationMapping field.
func (r *mutationResolver) CreateRiskMitigationMapping(ctx context.Context, input types.CreateRiskMitigationMappingInput) (*types.CreateRiskMitigationMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.RiskID.TenantID())
err := svc.Risks.CreateMapping(ctx, input.RiskID, input.MitigationID, input.Probability, input.Impact)
err := svc.Risks.CreateMitigationMapping(ctx, input.RiskID, input.MitigationID)
if err != nil {
panic(fmt.Errorf("cannot create risk mapping: %w", err))
panic(fmt.Errorf("cannot create risk mitigation mapping: %w", err))
}
return &types.CreateRiskMappingPayload{
return &types.CreateRiskMitigationMappingPayload{
Success: true,
}, nil
}
// DeleteRiskMapping is the resolver for the deleteRiskMapping field.
func (r *mutationResolver) DeleteRiskMapping(ctx context.Context, input types.DeleteRiskMappingInput) (*types.DeleteRiskMappingPayload, error) {
// DeleteRiskMitigationMapping is the resolver for the deleteRiskMitigationMapping field.
func (r *mutationResolver) DeleteRiskMitigationMapping(ctx context.Context, input types.DeleteRiskMitigationMappingInput) (*types.DeleteRiskMitigationMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.RiskID.TenantID())
err := svc.Risks.DeleteMapping(ctx, input.RiskID, input.MitigationID)
err := svc.Risks.DeleteMitigationMapping(ctx, input.RiskID, input.MitigationID)
if err != nil {
panic(fmt.Errorf("cannot delete risk mapping: %w", err))
panic(fmt.Errorf("cannot delete risk mitigation mapping: %w", err))
}
return &types.DeleteRiskMappingPayload{
return &types.DeleteRiskMitigationMappingPayload{
Success: true,
}, nil
}
// CreateRiskPolicyMapping is the resolver for the createRiskPolicyMapping field.
func (r *mutationResolver) CreateRiskPolicyMapping(ctx context.Context, input types.CreateRiskPolicyMappingInput) (*types.CreateRiskPolicyMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.RiskID.TenantID())
err := svc.Risks.CreatePolicyMapping(ctx, input.RiskID, input.PolicyID)
if err != nil {
panic(fmt.Errorf("cannot create risk policy mapping: %w", err))
}
return &types.CreateRiskPolicyMappingPayload{
Success: true,
}, nil
}
// DeleteRiskPolicyMapping is the resolver for the deleteRiskPolicyMapping field.
func (r *mutationResolver) DeleteRiskPolicyMapping(ctx context.Context, input types.DeleteRiskPolicyMappingInput) (*types.DeleteRiskPolicyMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.RiskID.TenantID())
err := svc.Risks.DeletePolicyMapping(ctx, input.RiskID, input.PolicyID)
if err != nil {
panic(fmt.Errorf("cannot delete risk policy mapping: %w", err))
}
return &types.DeleteRiskPolicyMappingPayload{
Success: true,
}, nil
}
@@ -1336,6 +1368,56 @@ func (r *riskResolver) Mitigations(ctx context.Context, obj *types.Risk, first *
return types.NewMitigationConnection(page), nil
}
// Policies is the resolver for the policies field.
func (r *riskResolver) Policies(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PolicyOrderBy) (*types.PolicyConnection, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.PolicyOrderField]{
Field: coredata.PolicyOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.PolicyOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Policies.ListForRiskID(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list risk policies: %w", err))
}
return types.NewPolicyConnection(page), nil
}
// Controls is the resolver for the controls field.
func (r *riskResolver) Controls(ctx context.Context, obj *types.Risk, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy) (*types.ControlConnection, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := svc.Controls.ListForRiskID(ctx, obj.ID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list risk controls: %w", err))
}
return types.NewControlConnection(page), nil
}
// AssignedTo is the resolver for the assignedTo field.
func (r *taskResolver) AssignedTo(ctx context.Context, obj *types.Task) (*types.People, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, obj.ID.TenantID())
@@ -1509,3 +1591,36 @@ type taskResolver struct{ *Resolver }
type vendorResolver struct{ *Resolver }
type vendorComplianceReportResolver struct{ *Resolver }
type viewerResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
/*
func (r *mutationResolver) CreateRiskControlMapping(ctx context.Context, input types.CreateRiskControlMappingInput) (*types.CreateRiskControlMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.RiskID.TenantID())
err := svc.Risks.CreateControlMapping(ctx, input.RiskID, input.ControlID)
if err != nil {
panic(fmt.Errorf("cannot create risk control mapping: %w", err))
}
return &types.CreateRiskControlMappingPayload{
Success: true,
}, nil
}
func (r *mutationResolver) DeleteRiskControlMapping(ctx context.Context, input types.DeleteRiskControlMappingInput) (*types.DeleteRiskControlMappingPayload, error) {
svc := r.GetTenantServiceIfAuthorized(ctx, input.RiskID.TenantID())
err := svc.Risks.DeleteControlMapping(ctx, input.RiskID, input.ControlID)
if err != nil {
panic(fmt.Errorf("cannot delete risk control mapping: %w", err))
}
return &types.DeleteRiskControlMappingPayload{
Success: true,
}, nil
}
*/