28
CHANGELOG.md
28
CHANGELOG.md
@@ -16,17 +16,35 @@ All notable changes to this project will be documented in this file.
|
||||
- New vendor data management system with comprehensive vendor information
|
||||
- Pre-populated vendor database with 12 common SaaS vendors and their certifications
|
||||
- Vendor details page with extended fields for improved vendor management:
|
||||
- Legal name and headquarters address
|
||||
- Website URL
|
||||
- Certification tracking with tag-based interface
|
||||
- Links to important vendor documents (SLA, DPA, security pages)
|
||||
- Support for multiple compliance certifications per vendor
|
||||
- Legal name and headquarters address
|
||||
- Website URL
|
||||
- Certification tracking with tag-based interface
|
||||
- Links to important vendor documents (SLA, DPA, security pages)
|
||||
- Support for multiple compliance certifications per vendor
|
||||
- Enhanced risk management with inherent and residual risk assessment capabilities
|
||||
- Added new fields to track both inherent and residual likelihood/impact values
|
||||
- Introduced risk severity calculation as the product of likelihood and impact
|
||||
- Added visual risk matrix to view risk distribution by severity
|
||||
- New risk-policy mapping functionality allowing risks to be linked to policies
|
||||
- New risk-control mapping functionality enabling risks to be linked to controls
|
||||
- Added edit functionality for risks with a new edit page
|
||||
- Pre-populated risk templates from a JSON data source
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated risk creation form to include both inherent and residual risk parameters
|
||||
- Improved risk list view with risk matrix visualization
|
||||
- Enhanced breadcrumb navigation for risk detail pages
|
||||
- Refactored risk-mitigation mapping to remove redundant probability/impact fields
|
||||
- Renamed probability field to likelihood for better alignment with risk management terminology
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix cannot create vendor when the name is too similar to suggested one
|
||||
- Fix UI showing double button to close evidence preview modal
|
||||
- Fix cannot delete vendor with compliance reports (added cascade delete constraint)
|
||||
- Improved license file formatting in vendors and risks data directories
|
||||
- Fixed URL in attribution text (`getprobo.com` → `www.getprobo.com`)
|
||||
|
||||
## [0.5.0] - 2025-04-10
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
24
apps/console/public/data/risks/LICENSE.md
Normal file
24
apps/console/public/data/risks/LICENSE.md
Normal 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/
|
||||
254
apps/console/public/data/risks/risks.json
Normal file
254
apps/console/public/data/risks/risks.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
4
apps/console/public/data/vendors/LICENSE.md
vendored
4
apps/console/public/data/vendors/LICENSE.md
vendored
@@ -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
|
||||
|
||||
29
apps/console/src/components/ui/switch.tsx
Normal file
29
apps/console/src/components/ui/switch.tsx
Normal 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 };
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
61
apps/console/src/pages/organizations/risks/EditRiskPage.tsx
Normal file
61
apps/console/src/pages/organizations/risks/EditRiskPage.tsx
Normal 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;
|
||||
404
apps/console/src/pages/organizations/risks/EditRiskView.tsx
Normal file
404
apps/console/src/pages/organizations/risks/EditRiskView.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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
184
apps/console/src/pages/organizations/risks/__generated__/EditRiskViewQuery.graphql.ts
generated
Normal file
184
apps/console/src/pages/organizations/risks/__generated__/EditRiskViewQuery.graphql.ts
generated
Normal 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;
|
||||
167
apps/console/src/pages/organizations/risks/__generated__/EditRiskViewUpdateRiskMutation.graphql.ts
generated
Normal file
167
apps/console/src/pages/organizations/risks/__generated__/EditRiskViewUpdateRiskMutation.graphql.ts
generated
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
188
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
7
pkg/coredata/migrations/20250411T212500Z.sql
Normal file
7
pkg/coredata/migrations/20250411T212500Z.sql
Normal 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;
|
||||
5
pkg/coredata/migrations/20250411T215600Z.sql
Normal file
5
pkg/coredata/migrations/20250411T215600Z.sql
Normal 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;
|
||||
2
pkg/coredata/migrations/20250411T225200Z.sql
Normal file
2
pkg/coredata/migrations/20250411T225200Z.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE risks RENAME COLUMN likelihood TO inherent_likelihood;
|
||||
ALTER TABLE risks RENAME COLUMN impact TO inherent_impact;
|
||||
7
pkg/coredata/migrations/20250411T233300Z.sql
Normal file
7
pkg/coredata/migrations/20250411T233300Z.sql
Normal 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)
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
95
pkg/coredata/risk_policy.go
Normal file
95
pkg/coredata/risk_policy.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user