diff --git a/packages/n8n-node/nodes/Probo/Probo.node.ts b/packages/n8n-node/nodes/Probo/Probo.node.ts index e2a374274..0b3df7cc8 100644 --- a/packages/n8n-node/nodes/Probo/Probo.node.ts +++ b/packages/n8n-node/nodes/Probo/Probo.node.ts @@ -186,6 +186,11 @@ export class Probo implements INodeType { value: 'risk', description: 'Manage risks', }, + { + name: 'Risk Assessment', + value: 'riskAssessment', + description: 'Manage risk assessments', + }, { name: 'Statement of Applicability', value: 'statementOfApplicability', diff --git a/packages/n8n-node/nodes/Probo/actions/index.ts b/packages/n8n-node/nodes/Probo/actions/index.ts index 60f27182a..e6f404f70 100644 --- a/packages/n8n-node/nodes/Probo/actions/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/index.ts @@ -35,6 +35,7 @@ import * as organization from './organization'; import * as organizationContext from './organizationContext'; import * as processingActivity from './processingActivity'; import * as rightsRequest from './rightsRequest'; +import * as riskAssessment from './riskAssessment'; import * as user from './user'; import * as risk from './risk'; import * as statementOfApplicability from './statementOfApplicability'; @@ -77,6 +78,7 @@ export const resources: Record = { organizationContext: organizationContext as ResourceModule, processingActivity: processingActivity as ResourceModule, rightsRequest: rightsRequest as ResourceModule, + riskAssessment: riskAssessment as ResourceModule, user: user as ResourceModule, risk: risk as ResourceModule, statementOfApplicability: statementOfApplicability as ResourceModule, diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/create.operation.ts new file mode 100644 index 000000000..58e9f7809 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/create.operation.ts @@ -0,0 +1,109 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['create'], + }, + }, + default: '', + description: 'The name of the risk assessment', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the risk assessment', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + description?: string; + }; + + const query = ` + mutation CreateRiskAssessment($input: CreateRiskAssessmentInput!) { + createRiskAssessment(input: $input) { + riskAssessmentEdge { + node { + id + name + description + createdAt + updatedAt + } + } + } + } + `; + + const input: Record = { + organizationId, + name, + }; + if (additionalFields.description) input.description = additionalFields.description; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/createNode.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createNode.operation.ts new file mode 100644 index 000000000..7b336e0f5 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createNode.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'riskAssessmentScopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createNode'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Node Type', + name: 'nodeType', + type: 'options', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createNode'], + }, + }, + options: [ + { + name: 'Entity', + value: 'ENTITY', + }, + { + name: 'Boundary', + value: 'BOUNDARY', + }, + { + name: 'Asset', + value: 'ASSET', + }, + { + name: 'Data', + value: 'DATA', + }, + ], + default: 'ENTITY', + description: 'The type of the node', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createNode'], + }, + }, + default: '', + description: 'The name of the node', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string; + const nodeType = this.getNodeParameter('nodeType', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + + const query = ` + mutation CreateRiskAssessmentNode($input: CreateRiskAssessmentNodeInput!) { + createRiskAssessmentNode(input: $input) { + riskAssessmentNodeEdge { + node { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScopeId, nodeType, name }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/createProcess.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createProcess.operation.ts new file mode 100644 index 000000000..ea6ea247e --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createProcess.operation.ts @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'riskAssessmentScopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createProcess'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Source Node ID', + name: 'sourceNodeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createProcess'], + }, + }, + default: '', + description: 'The ID of the source node', + required: true, + }, + { + displayName: 'Target Node ID', + name: 'targetNodeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createProcess'], + }, + }, + default: '', + description: 'The ID of the target node', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createProcess'], + }, + }, + default: '', + description: 'The name of the process', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string; + const sourceNodeId = this.getNodeParameter('sourceNodeId', itemIndex) as string; + const targetNodeId = this.getNodeParameter('targetNodeId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + + const query = ` + mutation CreateRiskAssessmentProcess($input: CreateRiskAssessmentProcessInput!) { + createRiskAssessmentProcess(input: $input) { + riskAssessmentProcessEdge { + node { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScopeId, sourceNodeId, targetNodeId, name }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/createScenario.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createScenario.operation.ts new file mode 100644 index 000000000..631001f5f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createScenario.operation.ts @@ -0,0 +1,110 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'riskAssessmentScopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createScenario'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createScenario'], + }, + }, + default: '', + description: 'The name of the scenario', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createScenario'], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the scenario', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + description?: string; + }; + + const query = ` + mutation CreateRiskAssessmentScenario($input: CreateRiskAssessmentScenarioInput!) { + createRiskAssessmentScenario(input: $input) { + riskAssessmentScenarioEdge { + node { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } + } + } + `; + + const input: Record = { + riskAssessmentScopeId, + name, + }; + if (additionalFields.description) input.description = additionalFields.description; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/createScope.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createScope.operation.ts new file mode 100644 index 000000000..cb0d6a29c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createScope.operation.ts @@ -0,0 +1,80 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Risk Assessment ID', + name: 'riskAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createScope'], + }, + }, + default: '', + description: 'The ID of the risk assessment', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createScope'], + }, + }, + default: '', + description: 'The name of the scope', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentId = this.getNodeParameter('riskAssessmentId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + + const query = ` + mutation CreateRiskAssessmentScope($input: CreateRiskAssessmentScopeInput!) { + createRiskAssessmentScope(input: $input) { + riskAssessmentScopeEdge { + node { + id + riskAssessmentId + name + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentId, name }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/createThreat.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createThreat.operation.ts new file mode 100644 index 000000000..3caa3065c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/createThreat.operation.ts @@ -0,0 +1,112 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'riskAssessmentScopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createThreat'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Process ID', + name: 'processId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createThreat'], + }, + }, + default: '', + description: 'The ID of the process', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createThreat'], + }, + }, + default: '', + description: 'The name of the threat', + required: true, + }, + { + displayName: 'Category', + name: 'category', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['createThreat'], + }, + }, + default: '', + description: 'The category of the threat', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string; + const processId = this.getNodeParameter('processId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const category = this.getNodeParameter('category', itemIndex) as string; + + const query = ` + mutation CreateRiskAssessmentThreat($input: CreateRiskAssessmentThreatInput!) { + createRiskAssessmentThreat(input: $input) { + riskAssessmentThreatEdge { + node { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScopeId, processId, name, category }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/delete.operation.ts new file mode 100644 index 000000000..83e391466 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Risk Assessment ID', + name: 'riskAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the risk assessment to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentId = this.getNodeParameter('riskAssessmentId', itemIndex) as string; + + const query = ` + mutation DeleteRiskAssessment($input: DeleteRiskAssessmentInput!) { + deleteRiskAssessment(input: $input) { + deletedRiskAssessmentId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { riskAssessmentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteNode.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteNode.operation.ts new file mode 100644 index 000000000..3877afed5 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteNode.operation.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Node ID', + name: 'nodeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['deleteNode'], + }, + }, + default: '', + description: 'The ID of the node to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const nodeId = this.getNodeParameter('nodeId', itemIndex) as string; + + const query = ` + mutation DeleteRiskAssessmentNode($input: DeleteRiskAssessmentNodeInput!) { + deleteRiskAssessmentNode(input: $input) { + deletedRiskAssessmentNodeId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentNodeId: nodeId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteProcess.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteProcess.operation.ts new file mode 100644 index 000000000..1bf5beaaa --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteProcess.operation.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Process ID', + name: 'processId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['deleteProcess'], + }, + }, + default: '', + description: 'The ID of the process to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processId = this.getNodeParameter('processId', itemIndex) as string; + + const query = ` + mutation DeleteRiskAssessmentProcess($input: DeleteRiskAssessmentProcessInput!) { + deleteRiskAssessmentProcess(input: $input) { + deletedRiskAssessmentProcessId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentProcessId: processId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteScenario.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteScenario.operation.ts new file mode 100644 index 000000000..d94d85178 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteScenario.operation.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'scenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['deleteScenario'], + }, + }, + default: '', + description: 'The ID of the scenario to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scenarioId = this.getNodeParameter('scenarioId', itemIndex) as string; + + const query = ` + mutation DeleteRiskAssessmentScenario($input: DeleteRiskAssessmentScenarioInput!) { + deleteRiskAssessmentScenario(input: $input) { + deletedRiskAssessmentScenarioId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScenarioId: scenarioId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteScope.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteScope.operation.ts new file mode 100644 index 000000000..83108e631 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteScope.operation.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['deleteScope'], + }, + }, + default: '', + description: 'The ID of the scope to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + + const query = ` + mutation DeleteRiskAssessmentScope($input: DeleteRiskAssessmentScopeInput!) { + deleteRiskAssessmentScope(input: $input) { + deletedRiskAssessmentScopeId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScopeId: scopeId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteThreat.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteThreat.operation.ts new file mode 100644 index 000000000..9de232a50 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/deleteThreat.operation.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Threat ID', + name: 'threatId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['deleteThreat'], + }, + }, + default: '', + description: 'The ID of the threat to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const threatId = this.getNodeParameter('threatId', itemIndex) as string; + + const query = ` + mutation DeleteRiskAssessmentThreat($input: DeleteRiskAssessmentThreatInput!) { + deleteRiskAssessmentThreat(input: $input) { + deletedRiskAssessmentThreatId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentThreatId: threatId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/get.operation.ts new file mode 100644 index 000000000..861342b13 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/get.operation.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Risk Assessment ID', + name: 'riskAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the risk assessment', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentId = this.getNodeParameter('riskAssessmentId', itemIndex) as string; + + const query = ` + query GetRiskAssessment($id: ID!) { + node(id: $id) { + ... on RiskAssessment { + id + name + description + createdAt + updatedAt + } + } + } + `; + + const variables = { + id: riskAssessmentId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAll.operation.ts new file mode 100644 index 000000000..18aa93685 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAll.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetRiskAssessments($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + riskAssessments(first: $first, after: $after) { + edges { + node { + id + name + description + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const riskAssessments = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.riskAssessments as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { riskAssessments }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllNodes.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllNodes.operation.ts new file mode 100644 index 000000000..105877420 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllNodes.operation.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllNodes'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllNodes'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllNodes'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetNodes($scopeId: ID!, $first: Int, $after: CursorKey) { + node(id: $scopeId) { + ... on RiskAssessmentScope { + nodes(first: $first, after: $after) { + edges { + node { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const nodes = await proboApiRequestAllItems.call( + this, + query, + { scopeId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.nodes as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { nodes }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllProcesses.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllProcesses.operation.ts new file mode 100644 index 000000000..ce890c060 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllProcesses.operation.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllProcesses'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllProcesses'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllProcesses'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetProcesses($scopeId: ID!, $first: Int, $after: CursorKey) { + node(id: $scopeId) { + ... on RiskAssessmentScope { + processes(first: $first, after: $after) { + edges { + node { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const processes = await proboApiRequestAllItems.call( + this, + query, + { scopeId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.processes as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { processes }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllScenarios.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllScenarios.operation.ts new file mode 100644 index 000000000..6dfd1844b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllScenarios.operation.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllScenarios'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllScenarios'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllScenarios'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetScenarios($scopeId: ID!, $first: Int, $after: CursorKey) { + node(id: $scopeId) { + ... on RiskAssessmentScope { + scenarios(first: $first, after: $after) { + edges { + node { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const scenarios = await proboApiRequestAllItems.call( + this, + query, + { scopeId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.scenarios as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { scenarios }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllScopes.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllScopes.operation.ts new file mode 100644 index 000000000..6dddb0cb0 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllScopes.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Risk Assessment ID', + name: 'riskAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllScopes'], + }, + }, + default: '', + description: 'The ID of the risk assessment', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllScopes'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllScopes'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentId = this.getNodeParameter('riskAssessmentId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetScopes($riskAssessmentId: ID!, $first: Int, $after: CursorKey) { + node(id: $riskAssessmentId) { + ... on RiskAssessment { + scopes(first: $first, after: $after) { + edges { + node { + id + riskAssessmentId + name + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const scopes = await proboApiRequestAllItems.call( + this, + query, + { riskAssessmentId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.scopes as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { scopes }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllThreats.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllThreats.operation.ts new file mode 100644 index 000000000..8ad689b0d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getAllThreats.operation.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllThreats'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllThreats'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getAllThreats'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetThreats($scopeId: ID!, $first: Int, $after: CursorKey) { + node(id: $scopeId) { + ... on RiskAssessmentScope { + threats(first: $first, after: $after) { + edges { + node { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const threats = await proboApiRequestAllItems.call( + this, + query, + { scopeId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.threats as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { threats }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getNode.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getNode.operation.ts new file mode 100644 index 000000000..58f0b134c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getNode.operation.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Node ID', + name: 'nodeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getNode'], + }, + }, + default: '', + description: 'The ID of the node', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const nodeId = this.getNodeParameter('nodeId', itemIndex) as string; + + const query = ` + query GetRiskAssessmentNode($id: ID!) { + node(id: $id) { + ... on RiskAssessmentNode { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { id: nodeId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getProcess.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getProcess.operation.ts new file mode 100644 index 000000000..19e64151e --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getProcess.operation.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Process ID', + name: 'processId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getProcess'], + }, + }, + default: '', + description: 'The ID of the process', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processId = this.getNodeParameter('processId', itemIndex) as string; + + const query = ` + query GetRiskAssessmentProcess($id: ID!) { + node(id: $id) { + ... on RiskAssessmentProcess { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { id: processId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScenario.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScenario.operation.ts new file mode 100644 index 000000000..d162c75b1 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScenario.operation.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'scenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getScenario'], + }, + }, + default: '', + description: 'The ID of the scenario', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scenarioId = this.getNodeParameter('scenarioId', itemIndex) as string; + + const query = ` + query GetRiskAssessmentScenario($id: ID!) { + node(id: $id) { + ... on RiskAssessmentScenario { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { id: scenarioId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScope.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScope.operation.ts new file mode 100644 index 000000000..3dfaf359b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScope.operation.ts @@ -0,0 +1,61 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getScope'], + }, + }, + default: '', + description: 'The ID of the scope', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + + const query = ` + query GetRiskAssessmentScope($id: ID!) { + node(id: $id) { + ... on RiskAssessmentScope { + id + riskAssessmentId + name + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { id: scopeId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScopeMermaidChart.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScopeMermaidChart.operation.ts new file mode 100644 index 000000000..e13de5681 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getScopeMermaidChart.operation.ts @@ -0,0 +1,59 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getScopeMermaidChart'], + }, + }, + default: '', + description: 'The ID of the risk assessment scope', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + + const query = ` + query GetScopeMermaidChart($id: ID!) { + node(id: $id) { + ... on RiskAssessmentScope { + id + name + mermaidChart + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { id: scopeId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/getThreat.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getThreat.operation.ts new file mode 100644 index 000000000..d79aae3e4 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/getThreat.operation.ts @@ -0,0 +1,63 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Threat ID', + name: 'threatId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['getThreat'], + }, + }, + default: '', + description: 'The ID of the threat', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const threatId = this.getNodeParameter('threatId', itemIndex) as string; + + const query = ` + query GetRiskAssessmentThreat($id: ID!) { + node(id: $id) { + ... on RiskAssessmentThreat { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { id: threatId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/index.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/index.ts new file mode 100644 index 000000000..df0156448 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/index.ts @@ -0,0 +1,345 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as createScopeOp from './createScope.operation'; +import * as getScopeOp from './getScope.operation'; +import * as getAllScopesOp from './getAllScopes.operation'; +import * as updateScopeOp from './updateScope.operation'; +import * as deleteScopeOp from './deleteScope.operation'; +import * as getScopeMermaidChartOp from './getScopeMermaidChart.operation'; +import * as createNodeOp from './createNode.operation'; +import * as getNodeOp from './getNode.operation'; +import * as getAllNodesOp from './getAllNodes.operation'; +import * as updateNodeOp from './updateNode.operation'; +import * as deleteNodeOp from './deleteNode.operation'; +import * as createProcessOp from './createProcess.operation'; +import * as getProcessOp from './getProcess.operation'; +import * as getAllProcessesOp from './getAllProcesses.operation'; +import * as updateProcessOp from './updateProcess.operation'; +import * as deleteProcessOp from './deleteProcess.operation'; +import * as createThreatOp from './createThreat.operation'; +import * as getThreatOp from './getThreat.operation'; +import * as getAllThreatsOp from './getAllThreats.operation'; +import * as updateThreatOp from './updateThreat.operation'; +import * as deleteThreatOp from './deleteThreat.operation'; +import * as createScenarioOp from './createScenario.operation'; +import * as getScenarioOp from './getScenario.operation'; +import * as getAllScenariosOp from './getAllScenarios.operation'; +import * as updateScenarioOp from './updateScenario.operation'; +import * as deleteScenarioOp from './deleteScenario.operation'; +import * as linkScenarioThreatOp from './linkScenarioThreat.operation'; +import * as unlinkScenarioThreatOp from './unlinkScenarioThreat.operation'; +import * as linkScenarioRiskOp from './linkScenarioRisk.operation'; +import * as unlinkScenarioRiskOp from './unlinkScenarioRisk.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['riskAssessment'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a risk assessment', + action: 'Create a risk assessment', + }, + { + name: 'Create Node', + value: 'createNode', + description: 'Create a node in a scope', + action: 'Create a node', + }, + { + name: 'Create Process', + value: 'createProcess', + description: 'Create a process in a scope', + action: 'Create a process', + }, + { + name: 'Create Scenario', + value: 'createScenario', + description: 'Create a scenario in a scope', + action: 'Create a scenario', + }, + { + name: 'Create Scope', + value: 'createScope', + description: 'Create a scope in a risk assessment', + action: 'Create a scope', + }, + { + name: 'Create Threat', + value: 'createThreat', + description: 'Create a threat in a scope', + action: 'Create a threat', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a risk assessment', + action: 'Delete a risk assessment', + }, + { + name: 'Delete Node', + value: 'deleteNode', + description: 'Delete a node', + action: 'Delete a node', + }, + { + name: 'Delete Process', + value: 'deleteProcess', + description: 'Delete a process', + action: 'Delete a process', + }, + { + name: 'Delete Scenario', + value: 'deleteScenario', + description: 'Delete a scenario', + action: 'Delete a scenario', + }, + { + name: 'Delete Scope', + value: 'deleteScope', + description: 'Delete a scope', + action: 'Delete a scope', + }, + { + name: 'Delete Threat', + value: 'deleteThreat', + description: 'Delete a threat', + action: 'Delete a threat', + }, + { + name: 'Get', + value: 'get', + description: 'Get a risk assessment', + action: 'Get a risk assessment', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many risk assessments', + action: 'Get many risk assessments', + }, + { + name: 'Get Many Nodes', + value: 'getAllNodes', + action: 'Get many nodes', + }, + { + name: 'Get Many Processes', + value: 'getAllProcesses', + action: 'Get many processes', + }, + { + name: 'Get Many Scenarios', + value: 'getAllScenarios', + action: 'Get many scenarios', + }, + { + name: 'Get Many Scopes', + value: 'getAllScopes', + action: 'Get many scopes', + }, + { + name: 'Get Many Threats', + value: 'getAllThreats', + action: 'Get many threats', + }, + { + name: 'Get Node', + value: 'getNode', + description: 'Get a node', + action: 'Get a node', + }, + { + name: 'Get Process', + value: 'getProcess', + description: 'Get a process', + action: 'Get a process', + }, + { + name: 'Get Scenario', + value: 'getScenario', + description: 'Get a scenario', + action: 'Get a scenario', + }, + { + name: 'Get Scope', + value: 'getScope', + description: 'Get a scope', + action: 'Get a scope', + }, + { + name: 'Get Scope Mermaid Chart', + value: 'getScopeMermaidChart', + description: 'Get the Mermaid diagram for a scope', + action: 'Get a scope mermaid chart', + }, + { + name: 'Get Threat', + value: 'getThreat', + description: 'Get a threat', + action: 'Get a threat', + }, + { + name: 'Link Scenario Risk', + value: 'linkScenarioRisk', + description: 'Link a scenario to a risk', + action: 'Link a scenario to a risk', + }, + { + name: 'Link Scenario Threat', + value: 'linkScenarioThreat', + description: 'Link a scenario to a threat', + action: 'Link a scenario to a threat', + }, + { + name: 'Unlink Scenario Risk', + value: 'unlinkScenarioRisk', + description: 'Unlink a scenario from a risk', + action: 'Unlink a scenario from a risk', + }, + { + name: 'Unlink Scenario Threat', + value: 'unlinkScenarioThreat', + description: 'Unlink a scenario from a threat', + action: 'Unlink a scenario from a threat', + }, + { + name: 'Update', + value: 'update', + description: 'Update a risk assessment', + action: 'Update a risk assessment', + }, + { + name: 'Update Node', + value: 'updateNode', + description: 'Update a node', + action: 'Update a node', + }, + { + name: 'Update Process', + value: 'updateProcess', + description: 'Update a process', + action: 'Update a process', + }, + { + name: 'Update Scenario', + value: 'updateScenario', + description: 'Update a scenario', + action: 'Update a scenario', + }, + { + name: 'Update Scope', + value: 'updateScope', + description: 'Update a scope', + action: 'Update a scope', + }, + { + name: 'Update Threat', + value: 'updateThreat', + description: 'Update a threat', + action: 'Update a threat', + }, + ], + default: 'create', + }, + ...createOp.description, + ...getOp.description, + ...getAllOp.description, + ...updateOp.description, + ...deleteOp.description, + ...createScopeOp.description, + ...getScopeOp.description, + ...getAllScopesOp.description, + ...updateScopeOp.description, + ...deleteScopeOp.description, + ...getScopeMermaidChartOp.description, + ...createNodeOp.description, + ...getNodeOp.description, + ...getAllNodesOp.description, + ...updateNodeOp.description, + ...deleteNodeOp.description, + ...createProcessOp.description, + ...getProcessOp.description, + ...getAllProcessesOp.description, + ...updateProcessOp.description, + ...deleteProcessOp.description, + ...createThreatOp.description, + ...getThreatOp.description, + ...getAllThreatsOp.description, + ...updateThreatOp.description, + ...deleteThreatOp.description, + ...createScenarioOp.description, + ...getScenarioOp.description, + ...getAllScenariosOp.description, + ...updateScenarioOp.description, + ...deleteScenarioOp.description, + ...linkScenarioThreatOp.description, + ...unlinkScenarioThreatOp.description, + ...linkScenarioRiskOp.description, + ...unlinkScenarioRiskOp.description, +]; + +export { + createOp as create, + getOp as get, + getAllOp as getAll, + updateOp as update, + deleteOp as delete, + createScopeOp as createScope, + getScopeOp as getScope, + getAllScopesOp as getAllScopes, + updateScopeOp as updateScope, + deleteScopeOp as deleteScope, + getScopeMermaidChartOp as getScopeMermaidChart, + createNodeOp as createNode, + getNodeOp as getNode, + getAllNodesOp as getAllNodes, + updateNodeOp as updateNode, + deleteNodeOp as deleteNode, + createProcessOp as createProcess, + getProcessOp as getProcess, + getAllProcessesOp as getAllProcesses, + updateProcessOp as updateProcess, + deleteProcessOp as deleteProcess, + createThreatOp as createThreat, + getThreatOp as getThreat, + getAllThreatsOp as getAllThreats, + updateThreatOp as updateThreat, + deleteThreatOp as deleteThreat, + createScenarioOp as createScenario, + getScenarioOp as getScenario, + getAllScenariosOp as getAllScenarios, + updateScenarioOp as updateScenario, + deleteScenarioOp as deleteScenario, + linkScenarioThreatOp as linkScenarioThreat, + unlinkScenarioThreatOp as unlinkScenarioThreat, + linkScenarioRiskOp as linkScenarioRisk, + unlinkScenarioRiskOp as unlinkScenarioRisk, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/linkScenarioRisk.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/linkScenarioRisk.operation.ts new file mode 100644 index 000000000..1c37c6cd7 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/linkScenarioRisk.operation.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'riskAssessmentScenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['linkScenarioRisk'], + }, + }, + default: '', + description: 'The ID of the scenario', + required: true, + }, + { + displayName: 'Risk ID', + name: 'riskId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['linkScenarioRisk'], + }, + }, + default: '', + description: 'The ID of the risk to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScenarioId = this.getNodeParameter('riskAssessmentScenarioId', itemIndex) as string; + const riskId = this.getNodeParameter('riskId', itemIndex) as string; + + const query = ` + mutation LinkRiskAssessmentScenarioRisk($input: LinkRiskAssessmentScenarioRiskInput!) { + linkRiskAssessmentScenarioRisk(input: $input) { + riskAssessmentScenario { + id + name + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScenarioId, riskId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/linkScenarioThreat.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/linkScenarioThreat.operation.ts new file mode 100644 index 000000000..775006b29 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/linkScenarioThreat.operation.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'riskAssessmentScenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['linkScenarioThreat'], + }, + }, + default: '', + description: 'The ID of the scenario', + required: true, + }, + { + displayName: 'Threat ID', + name: 'threatId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['linkScenarioThreat'], + }, + }, + default: '', + description: 'The ID of the threat to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScenarioId = this.getNodeParameter('riskAssessmentScenarioId', itemIndex) as string; + const threatId = this.getNodeParameter('threatId', itemIndex) as string; + + const query = ` + mutation LinkRiskAssessmentScenarioThreat($input: LinkRiskAssessmentScenarioThreatInput!) { + linkRiskAssessmentScenarioThreat(input: $input) { + riskAssessmentScenario { + id + name + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScenarioId, threatId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/unlinkScenarioRisk.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/unlinkScenarioRisk.operation.ts new file mode 100644 index 000000000..f88a55379 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/unlinkScenarioRisk.operation.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'riskAssessmentScenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['unlinkScenarioRisk'], + }, + }, + default: '', + description: 'The ID of the scenario', + required: true, + }, + { + displayName: 'Risk ID', + name: 'riskId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['unlinkScenarioRisk'], + }, + }, + default: '', + description: 'The ID of the risk to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScenarioId = this.getNodeParameter('riskAssessmentScenarioId', itemIndex) as string; + const riskId = this.getNodeParameter('riskId', itemIndex) as string; + + const query = ` + mutation UnlinkRiskAssessmentScenarioRisk($input: UnlinkRiskAssessmentScenarioRiskInput!) { + unlinkRiskAssessmentScenarioRisk(input: $input) { + riskAssessmentScenario { + id + name + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScenarioId, riskId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/unlinkScenarioThreat.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/unlinkScenarioThreat.operation.ts new file mode 100644 index 000000000..6680549ac --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/unlinkScenarioThreat.operation.ts @@ -0,0 +1,75 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'riskAssessmentScenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['unlinkScenarioThreat'], + }, + }, + default: '', + description: 'The ID of the scenario', + required: true, + }, + { + displayName: 'Threat ID', + name: 'threatId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['unlinkScenarioThreat'], + }, + }, + default: '', + description: 'The ID of the threat to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentScenarioId = this.getNodeParameter('riskAssessmentScenarioId', itemIndex) as string; + const threatId = this.getNodeParameter('threatId', itemIndex) as string; + + const query = ` + mutation UnlinkRiskAssessmentScenarioThreat($input: UnlinkRiskAssessmentScenarioThreatInput!) { + unlinkRiskAssessmentScenarioThreat(input: $input) { + riskAssessmentScenario { + id + name + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { riskAssessmentScenarioId, threatId }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/update.operation.ts new file mode 100644 index 000000000..74223e548 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/update.operation.ts @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Risk Assessment ID', + name: 'riskAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the risk assessment to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the risk assessment', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the risk assessment', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const riskAssessmentId = this.getNodeParameter('riskAssessmentId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + description?: string; + }; + + const query = ` + mutation UpdateRiskAssessment($input: UpdateRiskAssessmentInput!) { + updateRiskAssessment(input: $input) { + riskAssessment { + id + name + description + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: riskAssessmentId }; + if (additionalFields.name) input.name = additionalFields.name; + if (additionalFields.description !== undefined) input.description = additionalFields.description === '' ? null : additionalFields.description; + + if (Object.keys(input).length === 1) { + throw new Error('At least one field must be provided to update'); + } + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateNode.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateNode.operation.ts new file mode 100644 index 000000000..b042f73dc --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateNode.operation.ts @@ -0,0 +1,121 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Node ID', + name: 'nodeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateNode'], + }, + }, + default: '', + description: 'The ID of the node to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateNode'], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the node', + }, + { + displayName: 'Node Type', + name: 'nodeType', + type: 'options', + options: [ + { + name: 'Entity', + value: 'ENTITY', + }, + { + name: 'Boundary', + value: 'BOUNDARY', + }, + { + name: 'Asset', + value: 'ASSET', + }, + { + name: 'Data', + value: 'DATA', + }, + ], + default: 'ENTITY', + description: 'The type of the node', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const nodeId = this.getNodeParameter('nodeId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + nodeType?: string; + }; + + const query = ` + mutation UpdateRiskAssessmentNode($input: UpdateRiskAssessmentNodeInput!) { + updateRiskAssessmentNode(input: $input) { + riskAssessmentNode { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: nodeId }; + if (additionalFields.name) input.name = additionalFields.name; + if (additionalFields.nodeType) input.nodeType = additionalFields.nodeType; + + if (Object.keys(input).length === 1) { + throw new Error('At least one field must be provided to update'); + } + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateProcess.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateProcess.operation.ts new file mode 100644 index 000000000..638de80ce --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateProcess.operation.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Process ID', + name: 'processId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateProcess'], + }, + }, + default: '', + description: 'The ID of the process to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateProcess'], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the process', + }, + { + displayName: 'Source Node ID', + name: 'sourceNodeId', + type: 'string', + default: '', + description: 'The ID of the source node', + }, + { + displayName: 'Target Node ID', + name: 'targetNodeId', + type: 'string', + default: '', + description: 'The ID of the target node', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processId = this.getNodeParameter('processId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + sourceNodeId?: string; + targetNodeId?: string; + }; + + const query = ` + mutation UpdateRiskAssessmentProcess($input: UpdateRiskAssessmentProcessInput!) { + updateRiskAssessmentProcess(input: $input) { + riskAssessmentProcess { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: processId }; + if (additionalFields.name) input.name = additionalFields.name; + if (additionalFields.sourceNodeId) input.sourceNodeId = additionalFields.sourceNodeId; + if (additionalFields.targetNodeId) input.targetNodeId = additionalFields.targetNodeId; + + if (Object.keys(input).length === 1) { + throw new Error('At least one field must be provided to update'); + } + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateScenario.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateScenario.operation.ts new file mode 100644 index 000000000..222bf5d49 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateScenario.operation.ts @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scenario ID', + name: 'scenarioId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateScenario'], + }, + }, + default: '', + description: 'The ID of the scenario to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateScenario'], + }, + }, + options: [ + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the scenario', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the scenario', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scenarioId = this.getNodeParameter('scenarioId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + description?: string; + }; + + const query = ` + mutation UpdateRiskAssessmentScenario($input: UpdateRiskAssessmentScenarioInput!) { + updateRiskAssessmentScenario(input: $input) { + riskAssessmentScenario { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: scenarioId }; + if (additionalFields.name) input.name = additionalFields.name; + if (additionalFields.description !== undefined) input.description = additionalFields.description === '' ? null : additionalFields.description; + + if (Object.keys(input).length === 1) { + throw new Error('At least one field must be provided to update'); + } + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateScope.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateScope.operation.ts new file mode 100644 index 000000000..ec5c82687 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateScope.operation.ts @@ -0,0 +1,93 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Scope ID', + name: 'scopeId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateScope'], + }, + }, + default: '', + description: 'The ID of the scope to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateScope'], + }, + }, + options: [ + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the scope', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const scopeId = this.getNodeParameter('scopeId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + }; + + const query = ` + mutation UpdateRiskAssessmentScope($input: UpdateRiskAssessmentScopeInput!) { + updateRiskAssessmentScope(input: $input) { + riskAssessmentScope { + id + riskAssessmentId + name + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: scopeId }; + if (additionalFields.name) input.name = additionalFields.name; + + if (Object.keys(input).length === 1) { + throw new Error('At least one field must be provided to update'); + } + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateThreat.operation.ts b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateThreat.operation.ts new file mode 100644 index 000000000..e2c2b39c2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/riskAssessment/updateThreat.operation.ts @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Threat ID', + name: 'threatId', + type: 'string', + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateThreat'], + }, + }, + default: '', + description: 'The ID of the threat to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['riskAssessment'], + operation: ['updateThreat'], + }, + }, + options: [ + { + displayName: 'Category', + name: 'category', + type: 'string', + default: '', + description: 'The category of the threat', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'The name of the threat', + }, + { + displayName: 'Process ID', + name: 'processId', + type: 'string', + default: '', + description: 'The ID of the process', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const threatId = this.getNodeParameter('threatId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + name?: string; + processId?: string; + category?: string; + }; + + const query = ` + mutation UpdateRiskAssessmentThreat($input: UpdateRiskAssessmentThreatInput!) { + updateRiskAssessmentThreat(input: $input) { + riskAssessmentThreat { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: threatId }; + if (additionalFields.name) input.name = additionalFields.name; + if (additionalFields.processId) input.processId = additionalFields.processId; + if (additionalFields.category) input.category = additionalFields.category; + + if (Object.keys(input).length === 1) { + throw new Error('At least one field must be provided to update'); + } + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/pkg/cmd/risk-assessment/create/create.go b/pkg/cmd/risk-assessment/create/create.go new file mode 100644 index 000000000..0fa262c02 --- /dev/null +++ b/pkg/cmd/risk-assessment/create/create.go @@ -0,0 +1,154 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateRiskAssessmentInput!) { + createRiskAssessment(input: $input) { + riskAssessmentEdge { + node { + id + name + description + createdAt + updatedAt + } + } + } +} +` + +type createResponse struct { + CreateRiskAssessment struct { + RiskAssessmentEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` + } `json:"riskAssessmentEdge"` + } `json:"createRiskAssessment"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new risk assessment", + Example: ` # Create a risk assessment interactively + prb risk-assessment create + + # Create a risk assessment non-interactively + prb risk-assessment create --name "Annual Risk Assessment" --description "2026 annual review"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Risk assessment name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRiskAssessment.RiskAssessmentEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created risk assessment %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Risk assessment name (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Risk assessment description") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/delete/delete.go b/pkg/cmd/risk-assessment/delete/delete.go new file mode 100644 index 000000000..1f356dfd2 --- /dev/null +++ b/pkg/cmd/risk-assessment/delete/delete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteRiskAssessmentInput!) { + deleteRiskAssessment(input: $input) { + deletedRiskAssessmentId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a risk assessment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete risk assessment: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete risk assessment %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted risk assessment %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/list/list.go b/pkg/cmd/risk-assessment/list/list.go new file mode 100644 index 000000000..5ae95cfba --- /dev/null +++ b/pkg/cmd/risk-assessment/list/list.go @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: RiskAssessmentOrder) { + node(id: $id) { + __typename + ... on Organization { + riskAssessments(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + description + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type riskAssessment struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List risk assessments in an organization", + Aliases: []string{"ls"}, + Example: ` # List risk assessments in the default organization + prb risk-assessment list + + # List risk assessments sorted by name + prb risk-assessment ls --order-by NAME --json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + variables := map[string]any{ + "id": flagOrg, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + riskAssessments, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[riskAssessment], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + RiskAssessments api.Connection[riskAssessment] `json:"riskAssessments"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("organization %s not found", flagOrg) + } + + if resp.Node.Typename != "Organization" { + return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) + } + + return &resp.Node.RiskAssessments, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, riskAssessments) + } + + if len(riskAssessments) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No risk assessments found.") + return nil + } + + rows := make([][]string, 0, len(riskAssessments)) + for _, r := range riskAssessments { + desc := "" + if r.Description != nil { + desc = *r.Description + } + + rows = append(rows, []string{ + r.ID, + r.Name, + desc, + cmdutil.FormatTime(r.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "DESCRIPTION", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(riskAssessments) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d risk assessments\n", + len(riskAssessments), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of risk assessments to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/node/create/create.go b/pkg/cmd/risk-assessment/node/create/create.go new file mode 100644 index 000000000..9a28df383 --- /dev/null +++ b/pkg/cmd/risk-assessment/node/create/create.go @@ -0,0 +1,171 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateRiskAssessmentNodeInput!) { + createRiskAssessmentNode(input: $input) { + riskAssessmentNodeEdge { + node { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } + } +} +` + +type createResponse struct { + CreateRiskAssessmentNode struct { + RiskAssessmentNodeEdge struct { + Node struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + NodeType string `json:"nodeType"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` + } `json:"riskAssessmentNodeEdge"` + } `json:"createRiskAssessmentNode"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagScopeId string + flagNodeType string + flagName string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new risk assessment node", + Example: ` # Create a node interactively + prb risk-assessment node create --scope-id + + # Create a node non-interactively + prb risk-assessment node create --scope-id --node-type ASSET --name "Database server"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Node name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagNodeType == "" { + err := huh.NewSelect[string](). + Title("Node type"). + Options( + huh.NewOption("Entity", "ENTITY"), + huh.NewOption("Boundary", "BOUNDARY"), + huh.NewOption("Asset", "ASSET"), + huh.NewOption("Data", "DATA"), + ). + Value(&flagNodeType). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + if flagNodeType == "" { + return fmt.Errorf("node type is required; pass --node-type or run interactively") + } + + if err := cmdutil.ValidateEnum("node-type", flagNodeType, []string{"ENTITY", "BOUNDARY", "ASSET", "DATA"}); err != nil { + return err + } + + input := map[string]any{ + "riskAssessmentScopeId": flagScopeId, + "nodeType": flagNodeType, + "name": flagName, + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRiskAssessmentNode.RiskAssessmentNodeEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created risk assessment node %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScopeId, "scope-id", "", "Risk assessment scope ID (required)") + cmd.Flags().StringVar(&flagNodeType, "node-type", "", "Node type: ENTITY, BOUNDARY, ASSET, DATA (required)") + cmd.Flags().StringVar(&flagName, "name", "", "Node name (required)") + + _ = cmd.MarkFlagRequired("scope-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/node/delete/delete.go b/pkg/cmd/risk-assessment/node/delete/delete.go new file mode 100644 index 000000000..562ed8b20 --- /dev/null +++ b/pkg/cmd/risk-assessment/node/delete/delete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteRiskAssessmentNodeInput!) { + deleteRiskAssessmentNode(input: $input) { + deletedRiskAssessmentNodeId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a risk assessment node", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete risk assessment node: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete risk assessment node %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentNodeId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted risk assessment node %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/node/list/list.go b/pkg/cmd/risk-assessment/node/list/list.go new file mode 100644 index 000000000..bddfb05b1 --- /dev/null +++ b/pkg/cmd/risk-assessment/node/list/list.go @@ -0,0 +1,199 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: RiskAssessmentNodeOrder) { + node(id: $id) { + __typename + ... on RiskAssessmentScope { + nodes(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type riskAssessmentNode struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + NodeType string `json:"nodeType"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagScope string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List nodes in a risk assessment scope", + Aliases: []string{"ls"}, + Example: ` # List nodes in a scope + prb risk-assessment node list --scope + + # List nodes as JSON + prb risk-assessment node ls --scope --json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagScope == "" { + return fmt.Errorf("scope is required; pass --scope") + } + + variables := map[string]any{ + "id": flagScope, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + nodes, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[riskAssessmentNode], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Nodes api.Connection[riskAssessmentNode] `json:"nodes"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("scope %s not found", flagScope) + } + + if resp.Node.Typename != "RiskAssessmentScope" { + return nil, fmt.Errorf("expected RiskAssessmentScope node, got %s", resp.Node.Typename) + } + + return &resp.Node.Nodes, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, nodes) + } + + if len(nodes) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No nodes found.") + return nil + } + + rows := make([][]string, 0, len(nodes)) + for _, n := range nodes { + rows = append(rows, []string{ + n.ID, + n.Name, + n.NodeType, + cmdutil.FormatTime(n.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "TYPE", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(nodes) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d nodes\n", + len(nodes), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagScope, "scope", "", "Risk assessment scope ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of nodes to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + _ = cmd.MarkFlagRequired("scope") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/node/node.go b/pkg/cmd/risk-assessment/node/node.go new file mode 100644 index 000000000..dc7dd9fcf --- /dev/null +++ b/pkg/cmd/risk-assessment/node/node.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 node + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/risk-assessment/node/create" + "go.probo.inc/probo/pkg/cmd/risk-assessment/node/delete" + "go.probo.inc/probo/pkg/cmd/risk-assessment/node/list" + "go.probo.inc/probo/pkg/cmd/risk-assessment/node/update" + "go.probo.inc/probo/pkg/cmd/risk-assessment/node/view" +) + +func NewCmdNode(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "node ", + Short: "Manage risk assessment nodes", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/node/update/update.go b/pkg/cmd/risk-assessment/node/update/update.go new file mode 100644 index 000000000..e5b91e19a --- /dev/null +++ b/pkg/cmd/risk-assessment/node/update/update.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateRiskAssessmentNodeInput!) { + updateRiskAssessmentNode(input: $input) { + riskAssessmentNode { + id + nodeType + name + createdAt + updatedAt + } + } +} +` + +type updateResponse struct { + UpdateRiskAssessmentNode struct { + RiskAssessmentNode struct { + ID string `json:"id"` + NodeType string `json:"nodeType"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"riskAssessmentNode"` + } `json:"updateRiskAssessmentNode"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagNodeType string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a risk assessment node", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + + if cmd.Flags().Changed("node-type") { + if err := cmdutil.ValidateEnum("node-type", flagNodeType, []string{"ENTITY", "BOUNDARY", "ASSET", "DATA"}); err != nil { + return err + } + + input["nodeType"] = flagNodeType + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateRiskAssessmentNode.RiskAssessmentNode + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated risk assessment node %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Node name") + cmd.Flags().StringVar(&flagNodeType, "node-type", "", "Node type: ENTITY, BOUNDARY, ASSET, DATA") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/node/view/view.go b/pkg/cmd/risk-assessment/node/view/view.go new file mode 100644 index 000000000..cd8f90763 --- /dev/null +++ b/pkg/cmd/risk-assessment/node/view/view.go @@ -0,0 +1,133 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessmentNode { + id + riskAssessmentScopeId + nodeType + name + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + NodeType string `json:"nodeType"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a risk assessment node", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment node %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessmentNode" { + return fmt.Errorf("expected RiskAssessmentNode node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Scope:"), r.RiskAssessmentScopeId) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Node Type:"), r.NodeType) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/process/create/create.go b/pkg/cmd/risk-assessment/process/create/create.go new file mode 100644 index 000000000..ddaf4d0ba --- /dev/null +++ b/pkg/cmd/risk-assessment/process/create/create.go @@ -0,0 +1,154 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateRiskAssessmentProcessInput!) { + createRiskAssessmentProcess(input: $input) { + riskAssessmentProcessEdge { + node { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } + } +} +` + +type createResponse struct { + CreateRiskAssessmentProcess struct { + RiskAssessmentProcessEdge struct { + Node struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + SourceNodeId string `json:"sourceNodeId"` + TargetNodeId string `json:"targetNodeId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` + } `json:"riskAssessmentProcessEdge"` + } `json:"createRiskAssessmentProcess"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagScopeId string + flagSourceNodeId string + flagTargetNodeId string + flagName string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new risk assessment process", + Example: ` # Create a process interactively + prb risk-assessment process create --scope-id --source-node-id --target-node-id + + # Create a process non-interactively + prb risk-assessment process create --scope-id --source-node-id --target-node-id --name "Data flow"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Process name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "riskAssessmentScopeId": flagScopeId, + "sourceNodeId": flagSourceNodeId, + "targetNodeId": flagTargetNodeId, + "name": flagName, + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRiskAssessmentProcess.RiskAssessmentProcessEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created risk assessment process %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScopeId, "scope-id", "", "Risk assessment scope ID (required)") + cmd.Flags().StringVar(&flagSourceNodeId, "source-node-id", "", "Source node ID (required)") + cmd.Flags().StringVar(&flagTargetNodeId, "target-node-id", "", "Target node ID (required)") + cmd.Flags().StringVar(&flagName, "name", "", "Process name (required)") + + _ = cmd.MarkFlagRequired("scope-id") + _ = cmd.MarkFlagRequired("source-node-id") + _ = cmd.MarkFlagRequired("target-node-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/process/delete/delete.go b/pkg/cmd/risk-assessment/process/delete/delete.go new file mode 100644 index 000000000..a8598bbeb --- /dev/null +++ b/pkg/cmd/risk-assessment/process/delete/delete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteRiskAssessmentProcessInput!) { + deleteRiskAssessmentProcess(input: $input) { + deletedRiskAssessmentProcessId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a risk assessment process", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete risk assessment process: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete risk assessment process %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentProcessId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted risk assessment process %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/process/list/list.go b/pkg/cmd/risk-assessment/process/list/list.go new file mode 100644 index 000000000..877a4e51e --- /dev/null +++ b/pkg/cmd/risk-assessment/process/list/list.go @@ -0,0 +1,202 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: RiskAssessmentProcessOrder) { + node(id: $id) { + __typename + ... on RiskAssessmentScope { + processes(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type riskAssessmentProcess struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + SourceNodeId string `json:"sourceNodeId"` + TargetNodeId string `json:"targetNodeId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagScope string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List processes in a risk assessment scope", + Aliases: []string{"ls"}, + Example: ` # List processes in a scope + prb risk-assessment process list --scope + + # List processes as JSON + prb risk-assessment process ls --scope --json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagScope == "" { + return fmt.Errorf("scope is required; pass --scope") + } + + variables := map[string]any{ + "id": flagScope, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + processes, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[riskAssessmentProcess], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Processes api.Connection[riskAssessmentProcess] `json:"processes"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("scope %s not found", flagScope) + } + + if resp.Node.Typename != "RiskAssessmentScope" { + return nil, fmt.Errorf("expected RiskAssessmentScope node, got %s", resp.Node.Typename) + } + + return &resp.Node.Processes, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, processes) + } + + if len(processes) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No processes found.") + return nil + } + + rows := make([][]string, 0, len(processes)) + for _, p := range processes { + rows = append(rows, []string{ + p.ID, + p.Name, + p.SourceNodeId, + p.TargetNodeId, + cmdutil.FormatTime(p.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "SOURCE NODE", "TARGET NODE", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(processes) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d processes\n", + len(processes), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagScope, "scope", "", "Risk assessment scope ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of processes to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + _ = cmd.MarkFlagRequired("scope") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/process/process.go b/pkg/cmd/risk-assessment/process/process.go new file mode 100644 index 000000000..2590a2e53 --- /dev/null +++ b/pkg/cmd/risk-assessment/process/process.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 process + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/risk-assessment/process/create" + "go.probo.inc/probo/pkg/cmd/risk-assessment/process/delete" + "go.probo.inc/probo/pkg/cmd/risk-assessment/process/list" + "go.probo.inc/probo/pkg/cmd/risk-assessment/process/update" + "go.probo.inc/probo/pkg/cmd/risk-assessment/process/view" +) + +func NewCmdProcess(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "process ", + Short: "Manage risk assessment processes", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/process/update/update.go b/pkg/cmd/risk-assessment/process/update/update.go new file mode 100644 index 000000000..dc9f7f36d --- /dev/null +++ b/pkg/cmd/risk-assessment/process/update/update.go @@ -0,0 +1,134 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateRiskAssessmentProcessInput!) { + updateRiskAssessmentProcess(input: $input) { + riskAssessmentProcess { + id + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } +} +` + +type updateResponse struct { + UpdateRiskAssessmentProcess struct { + RiskAssessmentProcess struct { + ID string `json:"id"` + SourceNodeId string `json:"sourceNodeId"` + TargetNodeId string `json:"targetNodeId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"riskAssessmentProcess"` + } `json:"updateRiskAssessmentProcess"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagSourceNodeId string + flagTargetNodeId string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a risk assessment process", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + + if cmd.Flags().Changed("source-node-id") { + input["sourceNodeId"] = flagSourceNodeId + } + + if cmd.Flags().Changed("target-node-id") { + input["targetNodeId"] = flagTargetNodeId + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateRiskAssessmentProcess.RiskAssessmentProcess + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated risk assessment process %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Process name") + cmd.Flags().StringVar(&flagSourceNodeId, "source-node-id", "", "Source node ID") + cmd.Flags().StringVar(&flagTargetNodeId, "target-node-id", "", "Target node ID") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/process/view/view.go b/pkg/cmd/risk-assessment/process/view/view.go new file mode 100644 index 000000000..45a269388 --- /dev/null +++ b/pkg/cmd/risk-assessment/process/view/view.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessmentProcess { + id + riskAssessmentScopeId + sourceNodeId + targetNodeId + name + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + SourceNodeId string `json:"sourceNodeId"` + TargetNodeId string `json:"targetNodeId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a risk assessment process", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment process %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessmentProcess" { + return fmt.Errorf("expected RiskAssessmentProcess node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Scope:"), r.RiskAssessmentScopeId) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Source Node:"), r.SourceNodeId) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Target Node:"), r.TargetNodeId) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/risk_assessment.go b/pkg/cmd/risk-assessment/risk_assessment.go new file mode 100644 index 000000000..a35dedd57 --- /dev/null +++ b/pkg/cmd/risk-assessment/risk_assessment.go @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 riskassessment + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/risk-assessment/create" + "go.probo.inc/probo/pkg/cmd/risk-assessment/delete" + "go.probo.inc/probo/pkg/cmd/risk-assessment/list" + "go.probo.inc/probo/pkg/cmd/risk-assessment/node" + "go.probo.inc/probo/pkg/cmd/risk-assessment/process" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope" + "go.probo.inc/probo/pkg/cmd/risk-assessment/threat" + "go.probo.inc/probo/pkg/cmd/risk-assessment/update" + "go.probo.inc/probo/pkg/cmd/risk-assessment/view" +) + +func NewCmdRiskAssessment(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "risk-assessment ", + Short: "Manage risk assessments", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(scope.NewCmdScope(f)) + cmd.AddCommand(node.NewCmdNode(f)) + cmd.AddCommand(process.NewCmdProcess(f)) + cmd.AddCommand(threat.NewCmdThreat(f)) + cmd.AddCommand(scenario.NewCmdScenario(f)) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/create/create.go b/pkg/cmd/risk-assessment/scenario/create/create.go new file mode 100644 index 000000000..0e77c7659 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/create/create.go @@ -0,0 +1,150 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateRiskAssessmentScenarioInput!) { + createRiskAssessmentScenario(input: $input) { + riskAssessmentScenarioEdge { + node { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } + } +} +` + +type createResponse struct { + CreateRiskAssessmentScenario struct { + RiskAssessmentScenarioEdge struct { + Node struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` + } `json:"riskAssessmentScenarioEdge"` + } `json:"createRiskAssessmentScenario"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagScopeId string + flagName string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new risk assessment scenario", + Example: ` # Create a scenario interactively + prb risk-assessment scenario create --scope-id + + # Create a scenario non-interactively + prb risk-assessment scenario create --scope-id --name "Data breach scenario" --description "Unauthorized access to PII"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Scenario name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "riskAssessmentScopeId": flagScopeId, + "name": flagName, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRiskAssessmentScenario.RiskAssessmentScenarioEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created risk assessment scenario %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScopeId, "scope-id", "", "Risk assessment scope ID (required)") + cmd.Flags().StringVar(&flagName, "name", "", "Scenario name (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Scenario description") + + _ = cmd.MarkFlagRequired("scope-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/delete/delete.go b/pkg/cmd/risk-assessment/scenario/delete/delete.go new file mode 100644 index 000000000..bb965b3b2 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/delete/delete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteRiskAssessmentScenarioInput!) { + deleteRiskAssessmentScenario(input: $input) { + deletedRiskAssessmentScenarioId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a risk assessment scenario", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete risk assessment scenario: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete risk assessment scenario %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentScenarioId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted risk assessment scenario %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/link-risk/link_risk.go b/pkg/cmd/risk-assessment/scenario/link-risk/link_risk.go new file mode 100644 index 000000000..e80a0ceeb --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/link-risk/link_risk.go @@ -0,0 +1,94 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 linkrisk + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const linkRiskMutation = ` +mutation($input: LinkRiskAssessmentScenarioRiskInput!) { + linkRiskAssessmentScenarioRisk(input: $input) { + riskAssessmentScenario { + id + } + } +} +` + +func NewCmdLinkRisk(f *cmdutil.Factory) *cobra.Command { + var ( + flagScenarioId string + flagRiskId string + ) + + cmd := &cobra.Command{ + Use: "link-risk", + Short: "Link a risk to a risk assessment scenario", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + linkRiskMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentScenarioId": flagScenarioId, + "riskId": flagRiskId, + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Linked risk %s to scenario %s\n", + flagRiskId, + flagScenarioId, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScenarioId, "scenario-id", "", "Risk assessment scenario ID (required)") + cmd.Flags().StringVar(&flagRiskId, "risk-id", "", "Risk ID (required)") + + _ = cmd.MarkFlagRequired("scenario-id") + _ = cmd.MarkFlagRequired("risk-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/link-threat/link_threat.go b/pkg/cmd/risk-assessment/scenario/link-threat/link_threat.go new file mode 100644 index 000000000..7a741765d --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/link-threat/link_threat.go @@ -0,0 +1,94 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 linkthreat + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const linkThreatMutation = ` +mutation($input: LinkRiskAssessmentScenarioThreatInput!) { + linkRiskAssessmentScenarioThreat(input: $input) { + riskAssessmentScenario { + id + } + } +} +` + +func NewCmdLinkThreat(f *cmdutil.Factory) *cobra.Command { + var ( + flagScenarioId string + flagThreatId string + ) + + cmd := &cobra.Command{ + Use: "link-threat", + Short: "Link a threat to a risk assessment scenario", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + linkThreatMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentScenarioId": flagScenarioId, + "threatId": flagThreatId, + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Linked threat %s to scenario %s\n", + flagThreatId, + flagScenarioId, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScenarioId, "scenario-id", "", "Risk assessment scenario ID (required)") + cmd.Flags().StringVar(&flagThreatId, "threat-id", "", "Risk assessment threat ID (required)") + + _ = cmd.MarkFlagRequired("scenario-id") + _ = cmd.MarkFlagRequired("threat-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/list/list.go b/pkg/cmd/risk-assessment/scenario/list/list.go new file mode 100644 index 000000000..6c0aeaa09 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/list/list.go @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: RiskAssessmentScenarioOrder) { + node(id: $id) { + __typename + ... on RiskAssessmentScope { + scenarios(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type riskAssessmentScenario struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagScope string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List scenarios in a risk assessment scope", + Aliases: []string{"ls"}, + Example: ` # List scenarios in a scope + prb risk-assessment scenario list --scope + + # List scenarios as JSON + prb risk-assessment scenario ls --scope --json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagScope == "" { + return fmt.Errorf("scope is required; pass --scope") + } + + variables := map[string]any{ + "id": flagScope, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + scenarios, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[riskAssessmentScenario], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Scenarios api.Connection[riskAssessmentScenario] `json:"scenarios"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("scope %s not found", flagScope) + } + + if resp.Node.Typename != "RiskAssessmentScope" { + return nil, fmt.Errorf("expected RiskAssessmentScope node, got %s", resp.Node.Typename) + } + + return &resp.Node.Scenarios, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, scenarios) + } + + if len(scenarios) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No scenarios found.") + return nil + } + + rows := make([][]string, 0, len(scenarios)) + for _, s := range scenarios { + desc := "" + if s.Description != nil { + desc = *s.Description + } + + rows = append(rows, []string{ + s.ID, + s.Name, + desc, + cmdutil.FormatTime(s.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "DESCRIPTION", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(scenarios) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d scenarios\n", + len(scenarios), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagScope, "scope", "", "Risk assessment scope ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of scenarios to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + _ = cmd.MarkFlagRequired("scope") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/scenario.go b/pkg/cmd/risk-assessment/scenario/scenario.go new file mode 100644 index 000000000..296a6e597 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/scenario.go @@ -0,0 +1,48 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 scenario + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/create" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/delete" + linkrisk "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/link-risk" + linkthreat "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/link-threat" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/list" + unlinkrisk "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/unlink-risk" + unlinkthreat "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/unlink-threat" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/update" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scenario/view" +) + +func NewCmdScenario(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "scenario ", + Short: "Manage risk assessment scenarios", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(linkthreat.NewCmdLinkThreat(f)) + cmd.AddCommand(unlinkthreat.NewCmdUnlinkThreat(f)) + cmd.AddCommand(linkrisk.NewCmdLinkRisk(f)) + cmd.AddCommand(unlinkrisk.NewCmdUnlinkRisk(f)) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/unlink-risk/unlink_risk.go b/pkg/cmd/risk-assessment/scenario/unlink-risk/unlink_risk.go new file mode 100644 index 000000000..01ded5a7b --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/unlink-risk/unlink_risk.go @@ -0,0 +1,94 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 unlinkrisk + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const unlinkRiskMutation = ` +mutation($input: UnlinkRiskAssessmentScenarioRiskInput!) { + unlinkRiskAssessmentScenarioRisk(input: $input) { + riskAssessmentScenario { + id + } + } +} +` + +func NewCmdUnlinkRisk(f *cmdutil.Factory) *cobra.Command { + var ( + flagScenarioId string + flagRiskId string + ) + + cmd := &cobra.Command{ + Use: "unlink-risk", + Short: "Unlink a risk from a risk assessment scenario", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + unlinkRiskMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentScenarioId": flagScenarioId, + "riskId": flagRiskId, + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Unlinked risk %s from scenario %s\n", + flagRiskId, + flagScenarioId, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScenarioId, "scenario-id", "", "Risk assessment scenario ID (required)") + cmd.Flags().StringVar(&flagRiskId, "risk-id", "", "Risk ID (required)") + + _ = cmd.MarkFlagRequired("scenario-id") + _ = cmd.MarkFlagRequired("risk-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/unlink-threat/unlink_threat.go b/pkg/cmd/risk-assessment/scenario/unlink-threat/unlink_threat.go new file mode 100644 index 000000000..474a07ee8 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/unlink-threat/unlink_threat.go @@ -0,0 +1,94 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 unlinkthreat + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const unlinkThreatMutation = ` +mutation($input: UnlinkRiskAssessmentScenarioThreatInput!) { + unlinkRiskAssessmentScenarioThreat(input: $input) { + riskAssessmentScenario { + id + } + } +} +` + +func NewCmdUnlinkThreat(f *cmdutil.Factory) *cobra.Command { + var ( + flagScenarioId string + flagThreatId string + ) + + cmd := &cobra.Command{ + Use: "unlink-threat", + Short: "Unlink a threat from a risk assessment scenario", + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + unlinkThreatMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentScenarioId": flagScenarioId, + "threatId": flagThreatId, + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Unlinked threat %s from scenario %s\n", + flagThreatId, + flagScenarioId, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScenarioId, "scenario-id", "", "Risk assessment scenario ID (required)") + cmd.Flags().StringVar(&flagThreatId, "threat-id", "", "Risk assessment threat ID (required)") + + _ = cmd.MarkFlagRequired("scenario-id") + _ = cmd.MarkFlagRequired("threat-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/update/update.go b/pkg/cmd/risk-assessment/scenario/update/update.go new file mode 100644 index 000000000..005a2a2d0 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/update/update.go @@ -0,0 +1,126 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateRiskAssessmentScenarioInput!) { + updateRiskAssessmentScenario(input: $input) { + riskAssessmentScenario { + id + name + description + createdAt + updatedAt + } + } +} +` + +type updateResponse struct { + UpdateRiskAssessmentScenario struct { + RiskAssessmentScenario struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"riskAssessmentScenario"` + } `json:"updateRiskAssessmentScenario"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a risk assessment scenario", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateRiskAssessmentScenario.RiskAssessmentScenario + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated risk assessment scenario %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Scenario name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Scenario description") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scenario/view/view.go b/pkg/cmd/risk-assessment/scenario/view/view.go new file mode 100644 index 000000000..974fc4db2 --- /dev/null +++ b/pkg/cmd/risk-assessment/scenario/view/view.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessmentScenario { + id + riskAssessmentScopeId + name + description + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a risk assessment scenario", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment scenario %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessmentScenario" { + return fmt.Errorf("expected RiskAssessmentScenario node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Scope:"), r.RiskAssessmentScopeId) + + if r.Description != nil && *r.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *r.Description) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/create/create.go b/pkg/cmd/risk-assessment/scope/create/create.go new file mode 100644 index 000000000..6f08a9e74 --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/create/create.go @@ -0,0 +1,142 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateRiskAssessmentScopeInput!) { + createRiskAssessmentScope(input: $input) { + riskAssessmentScopeEdge { + node { + id + riskAssessmentId + name + createdAt + updatedAt + } + } + } +} +` + +type createResponse struct { + CreateRiskAssessmentScope struct { + RiskAssessmentScopeEdge struct { + Node struct { + ID string `json:"id"` + RiskAssessmentId string `json:"riskAssessmentId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` + } `json:"riskAssessmentScopeEdge"` + } `json:"createRiskAssessmentScope"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagRiskAssessmentId string + flagName string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new risk assessment scope", + Example: ` # Create a scope interactively + prb risk-assessment scope create --risk-assessment-id + + # Create a scope non-interactively + prb risk-assessment scope create --risk-assessment-id --name "Network scope"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Scope name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "riskAssessmentId": flagRiskAssessmentId, + "name": flagName, + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRiskAssessmentScope.RiskAssessmentScopeEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created risk assessment scope %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagRiskAssessmentId, "risk-assessment-id", "", "Risk assessment ID (required)") + cmd.Flags().StringVar(&flagName, "name", "", "Scope name (required)") + + _ = cmd.MarkFlagRequired("risk-assessment-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/delete/delete.go b/pkg/cmd/risk-assessment/scope/delete/delete.go new file mode 100644 index 000000000..c595284b3 --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/delete/delete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteRiskAssessmentScopeInput!) { + deleteRiskAssessmentScope(input: $input) { + deletedRiskAssessmentScopeId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a risk assessment scope", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete risk assessment scope: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete risk assessment scope %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentScopeId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted risk assessment scope %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/list/list.go b/pkg/cmd/risk-assessment/scope/list/list.go new file mode 100644 index 000000000..74e148e0c --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/list/list.go @@ -0,0 +1,196 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: RiskAssessmentScopeOrder) { + node(id: $id) { + __typename + ... on RiskAssessment { + scopes(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + riskAssessmentId + name + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type riskAssessmentScope struct { + ID string `json:"id"` + RiskAssessmentId string `json:"riskAssessmentId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagRiskAssessment string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List scopes in a risk assessment", + Aliases: []string{"ls"}, + Example: ` # List scopes for a risk assessment + prb risk-assessment scope list --risk-assessment + + # List scopes as JSON + prb risk-assessment scope ls --risk-assessment --json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagRiskAssessment == "" { + return fmt.Errorf("risk assessment is required; pass --risk-assessment") + } + + variables := map[string]any{ + "id": flagRiskAssessment, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + scopes, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[riskAssessmentScope], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Scopes api.Connection[riskAssessmentScope] `json:"scopes"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("risk assessment %s not found", flagRiskAssessment) + } + + if resp.Node.Typename != "RiskAssessment" { + return nil, fmt.Errorf("expected RiskAssessment node, got %s", resp.Node.Typename) + } + + return &resp.Node.Scopes, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, scopes) + } + + if len(scopes) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No scopes found.") + return nil + } + + rows := make([][]string, 0, len(scopes)) + for _, s := range scopes { + rows = append(rows, []string{ + s.ID, + s.Name, + cmdutil.FormatTime(s.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(scopes) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d scopes\n", + len(scopes), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagRiskAssessment, "risk-assessment", "", "Risk assessment ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of scopes to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + _ = cmd.MarkFlagRequired("risk-assessment") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/mermaid/mermaid.go b/pkg/cmd/risk-assessment/scope/mermaid/mermaid.go new file mode 100644 index 000000000..92859cb41 --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/mermaid/mermaid.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 mermaid + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const mermaidQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessmentScope { + id + name + mermaidChart + } + } +} +` + +type mermaidResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + MermaidChart string `json:"mermaidChart"` + } `json:"node"` +} + +func NewCmdMermaid(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "mermaid ", + Short: "Get the Mermaid chart for a risk assessment scope", + Example: ` # Print the Mermaid chart for a scope + prb risk-assessment scope mermaid + + # Output as JSON + prb risk-assessment scope mermaid --json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + mermaidQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp mermaidResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment scope %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessmentScope" { + return fmt.Errorf("expected RiskAssessmentScope node, got %s", resp.Node.Typename) + } + + _, _ = fmt.Fprintln(f.IOStreams.Out, resp.Node.MermaidChart) + + return nil + }, + } + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/scope.go b/pkg/cmd/risk-assessment/scope/scope.go new file mode 100644 index 000000000..d298627a9 --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/scope.go @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 scope + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope/create" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope/delete" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope/list" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope/mermaid" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope/update" + "go.probo.inc/probo/pkg/cmd/risk-assessment/scope/view" +) + +func NewCmdScope(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "scope ", + Short: "Manage risk assessment scopes", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(mermaid.NewCmdMermaid(f)) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/update/update.go b/pkg/cmd/risk-assessment/scope/update/update.go new file mode 100644 index 000000000..c5d917654 --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/update/update.go @@ -0,0 +1,116 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateRiskAssessmentScopeInput!) { + updateRiskAssessmentScope(input: $input) { + riskAssessmentScope { + id + name + createdAt + updatedAt + } + } +} +` + +type updateResponse struct { + UpdateRiskAssessmentScope struct { + RiskAssessmentScope struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"riskAssessmentScope"` + } `json:"updateRiskAssessmentScope"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var flagName string + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a risk assessment scope", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateRiskAssessmentScope.RiskAssessmentScope + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated risk assessment scope %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Scope name") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/scope/view/view.go b/pkg/cmd/risk-assessment/scope/view/view.go new file mode 100644 index 000000000..bf9efb1aa --- /dev/null +++ b/pkg/cmd/risk-assessment/scope/view/view.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessmentScope { + id + riskAssessmentId + name + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + RiskAssessmentId string `json:"riskAssessmentId"` + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a risk assessment scope", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment scope %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessmentScope" { + return fmt.Errorf("expected RiskAssessmentScope node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Risk Assessment:"), r.RiskAssessmentId) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/threat/create/create.go b/pkg/cmd/risk-assessment/threat/create/create.go new file mode 100644 index 000000000..5c5641449 --- /dev/null +++ b/pkg/cmd/risk-assessment/threat/create/create.go @@ -0,0 +1,167 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateRiskAssessmentThreatInput!) { + createRiskAssessmentThreat(input: $input) { + riskAssessmentThreatEdge { + node { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } + } +} +` + +type createResponse struct { + CreateRiskAssessmentThreat struct { + RiskAssessmentThreatEdge struct { + Node struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + ProcessId string `json:"processId"` + Name string `json:"name"` + Category string `json:"category"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` + } `json:"riskAssessmentThreatEdge"` + } `json:"createRiskAssessmentThreat"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagScopeId string + flagProcessId string + flagName string + flagCategory string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new risk assessment threat", + Example: ` # Create a threat interactively + prb risk-assessment threat create --scope-id --process-id + + # Create a threat non-interactively + prb risk-assessment threat create --scope-id --process-id --name "SQL injection" --category "Application"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Threat name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagCategory == "" { + err := huh.NewInput(). + Title("Threat category"). + Value(&flagCategory). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + if flagCategory == "" { + return fmt.Errorf("category is required; pass --category or run interactively") + } + + input := map[string]any{ + "riskAssessmentScopeId": flagScopeId, + "processId": flagProcessId, + "name": flagName, + "category": flagCategory, + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRiskAssessmentThreat.RiskAssessmentThreatEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created risk assessment threat %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagScopeId, "scope-id", "", "Risk assessment scope ID (required)") + cmd.Flags().StringVar(&flagProcessId, "process-id", "", "Process ID (required)") + cmd.Flags().StringVar(&flagName, "name", "", "Threat name (required)") + cmd.Flags().StringVar(&flagCategory, "category", "", "Threat category (required)") + + _ = cmd.MarkFlagRequired("scope-id") + _ = cmd.MarkFlagRequired("process-id") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/threat/delete/delete.go b/pkg/cmd/risk-assessment/threat/delete/delete.go new file mode 100644 index 000000000..790b914dc --- /dev/null +++ b/pkg/cmd/risk-assessment/threat/delete/delete.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteRiskAssessmentThreatInput!) { + deleteRiskAssessmentThreat(input: $input) { + deletedRiskAssessmentThreatId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a risk assessment threat", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete risk assessment threat: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete risk assessment threat %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "riskAssessmentThreatId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted risk assessment threat %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/threat/list/list.go b/pkg/cmd/risk-assessment/threat/list/list.go new file mode 100644 index 000000000..dd61744f2 --- /dev/null +++ b/pkg/cmd/risk-assessment/threat/list/list.go @@ -0,0 +1,202 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: RiskAssessmentThreatOrder) { + node(id: $id) { + __typename + ... on RiskAssessmentScope { + threats(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type riskAssessmentThreat struct { + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + ProcessId string `json:"processId"` + Name string `json:"name"` + Category string `json:"category"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagScope string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List threats in a risk assessment scope", + Aliases: []string{"ls"}, + Example: ` # List threats in a scope + prb risk-assessment threat list --scope + + # List threats as JSON + prb risk-assessment threat ls --scope --json`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagScope == "" { + return fmt.Errorf("scope is required; pass --scope") + } + + variables := map[string]any{ + "id": flagScope, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"CREATED_AT", "NAME"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + threats, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[riskAssessmentThreat], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Threats api.Connection[riskAssessmentThreat] `json:"threats"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("scope %s not found", flagScope) + } + + if resp.Node.Typename != "RiskAssessmentScope" { + return nil, fmt.Errorf("expected RiskAssessmentScope node, got %s", resp.Node.Typename) + } + + return &resp.Node.Threats, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, threats) + } + + if len(threats) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No threats found.") + return nil + } + + rows := make([][]string, 0, len(threats)) + for _, t := range threats { + rows = append(rows, []string{ + t.ID, + t.Name, + t.Category, + t.ProcessId, + cmdutil.FormatTime(t.CreatedAt), + }) + } + + tbl := cmdutil.NewTable("ID", "NAME", "CATEGORY", "PROCESS", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, tbl) + + if totalCount > len(threats) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d threats\n", + len(threats), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagScope, "scope", "", "Risk assessment scope ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of threats to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + _ = cmd.MarkFlagRequired("scope") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/threat/threat.go b/pkg/cmd/risk-assessment/threat/threat.go new file mode 100644 index 000000000..bf1f3766d --- /dev/null +++ b/pkg/cmd/risk-assessment/threat/threat.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 threat + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/risk-assessment/threat/create" + "go.probo.inc/probo/pkg/cmd/risk-assessment/threat/delete" + "go.probo.inc/probo/pkg/cmd/risk-assessment/threat/list" + "go.probo.inc/probo/pkg/cmd/risk-assessment/threat/update" + "go.probo.inc/probo/pkg/cmd/risk-assessment/threat/view" +) + +func NewCmdThreat(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "threat ", + Short: "Manage risk assessment threats", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/threat/update/update.go b/pkg/cmd/risk-assessment/threat/update/update.go new file mode 100644 index 000000000..2a1305f28 --- /dev/null +++ b/pkg/cmd/risk-assessment/threat/update/update.go @@ -0,0 +1,134 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateRiskAssessmentThreatInput!) { + updateRiskAssessmentThreat(input: $input) { + riskAssessmentThreat { + id + processId + name + category + createdAt + updatedAt + } + } +} +` + +type updateResponse struct { + UpdateRiskAssessmentThreat struct { + RiskAssessmentThreat struct { + ID string `json:"id"` + ProcessId string `json:"processId"` + Name string `json:"name"` + Category string `json:"category"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"riskAssessmentThreat"` + } `json:"updateRiskAssessmentThreat"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagCategory string + flagProcessId string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a risk assessment threat", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + + if cmd.Flags().Changed("category") { + input["category"] = flagCategory + } + + if cmd.Flags().Changed("process-id") { + input["processId"] = flagProcessId + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateRiskAssessmentThreat.RiskAssessmentThreat + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated risk assessment threat %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Threat name") + cmd.Flags().StringVar(&flagCategory, "category", "", "Threat category") + cmd.Flags().StringVar(&flagProcessId, "process-id", "", "Process ID") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/threat/view/view.go b/pkg/cmd/risk-assessment/threat/view/view.go new file mode 100644 index 000000000..4b5d7e144 --- /dev/null +++ b/pkg/cmd/risk-assessment/threat/view/view.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessmentThreat { + id + riskAssessmentScopeId + processId + name + category + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + RiskAssessmentScopeId string `json:"riskAssessmentScopeId"` + ProcessId string `json:"processId"` + Name string `json:"name"` + Category string `json:"category"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a risk assessment threat", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment threat %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessmentThreat" { + return fmt.Errorf("expected RiskAssessmentThreat node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Scope:"), r.RiskAssessmentScopeId) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Process:"), r.ProcessId) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Category:"), r.Category) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/risk-assessment/update/update.go b/pkg/cmd/risk-assessment/update/update.go new file mode 100644 index 000000000..d31c2e9b8 --- /dev/null +++ b/pkg/cmd/risk-assessment/update/update.go @@ -0,0 +1,126 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateRiskAssessmentInput!) { + updateRiskAssessment(input: $input) { + riskAssessment { + id + name + description + createdAt + updatedAt + } + } +} +` + +type updateResponse struct { + UpdateRiskAssessment struct { + RiskAssessment struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"riskAssessment"` + } `json:"updateRiskAssessment"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a risk assessment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.UpdateRiskAssessment.RiskAssessment + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated risk assessment %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Risk assessment name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Risk assessment description") + + return cmd +} diff --git a/pkg/cmd/risk-assessment/view/view.go b/pkg/cmd/risk-assessment/view/view.go new file mode 100644 index 000000000..3d1c3f5a9 --- /dev/null +++ b/pkg/cmd/risk-assessment/view/view.go @@ -0,0 +1,133 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RiskAssessment { + id + name + description + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a risk assessment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("risk assessment %s not found", args[0]) + } + + if resp.Node.Typename != "RiskAssessment" { + return fmt.Errorf("expected RiskAssessment node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + + if r.Description != nil && *r.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *r.Description) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 06919ff0a..e57af280a 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -43,6 +43,7 @@ import ( processingactivity "go.probo.inc/probo/pkg/cmd/processing-activity" rightsrequest "go.probo.inc/probo/pkg/cmd/rights-request" "go.probo.inc/probo/pkg/cmd/risk" + riskassessment "go.probo.inc/probo/pkg/cmd/risk-assessment" "go.probo.inc/probo/pkg/cmd/scim" "go.probo.inc/probo/pkg/cmd/soa" "go.probo.inc/probo/pkg/cmd/task" @@ -116,6 +117,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(processingactivity.NewCmdProcessingActivity(f)) cmd.AddCommand(rightsrequest.NewCmdRightsRequest(f)) cmd.AddCommand(risk.NewCmdRisk(f)) + cmd.AddCommand(riskassessment.NewCmdRiskAssessment(f)) cmd.AddCommand(scim.NewCmdScim(f)) cmd.AddCommand(soa.NewCmdSoa(f)) cmd.AddCommand(task.NewCmdTask(f)) diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index beba99c70..07d673bb4 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -212,6 +212,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.IAM, cfg.AccessReview, cfg.CookieBanner, + cfg.RiskManagement, cfg.TokenSecret, ), slackHandler: slack_v1.NewMux( diff --git a/pkg/server/api/mcp/v1/resolver.go b/pkg/server/api/mcp/v1/resolver.go index 171e6a52c..30d4a10e9 100644 --- a/pkg/server/api/mcp/v1/resolver.go +++ b/pkg/server/api/mcp/v1/resolver.go @@ -28,15 +28,17 @@ import ( "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/prosemirror" + "go.probo.inc/probo/pkg/riskmanagement" "go.probo.inc/probo/pkg/server/api/authn" ) type Resolver struct { - proboSvc *probo.Service - iamSvc *iam.Service - accessReview *accessreview.Service - cookieBanner *cookiebanner.Service - logger *log.Logger + proboSvc *probo.Service + iamSvc *iam.Service + accessReview *accessreview.Service + cookieBanner *cookiebanner.Service + riskManagement *riskmanagement.Service + logger *log.Logger } func markdownToProseMirrorJSON(markdown string) (string, error) { diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 3efbb077d..39afb05a7 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -20,6 +20,7 @@ import ( "go.probo.inc/probo/pkg/mail" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/probo" + "go.probo.inc/probo/pkg/riskmanagement" "go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/mcp/v1/types" ) @@ -5591,3 +5592,642 @@ func (r *Resolver) MoveTrackerResourceToCategoryTool(ctx context.Context, req *m return nil, types.MoveTrackerResourceToCategoryOutput{TrackerResource: types.NewTrackerResource(result.TrackerResource)}, nil } + +func (r *Resolver) ListRiskAssessmentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentsInput) (*mcp.CallToolResult, types.ListRiskAssessmentsOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionRiskAssessmentList) + + scope := coredata.NewScopeFromObjectID(input.OrganizationID) + + pageOrderBy := page.OrderBy[coredata.RiskAssessmentOrderField]{ + Field: coredata.RiskAssessmentOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskAssessmentOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.riskManagement.ListForOrganizationID(ctx, scope, input.OrganizationID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list risk assessments: %w", err)) + } + + return nil, types.NewListRiskAssessmentsOutput(p), nil +} + +func (r *Resolver) GetRiskAssessmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentInput) (*mcp.CallToolResult, types.GetRiskAssessmentOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + ra, err := r.riskManagement.Get(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentOutput{}, fmt.Errorf("failed to get risk assessment: %w", err) + } + + return nil, types.GetRiskAssessmentOutput{ + RiskAssessment: types.NewRiskAssessment(ra), + }, nil +} + +func (r *Resolver) AddRiskAssessmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentInput) (*mcp.CallToolResult, types.AddRiskAssessmentOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionRiskAssessmentCreate) + + scope := coredata.NewScopeFromObjectID(input.OrganizationID) + + ra, err := r.riskManagement.Create(ctx, scope, riskmanagement.CreateRiskAssessmentRequest{ + OrganizationID: input.OrganizationID, + Name: input.Name, + Description: input.Description, + }) + if err != nil { + return nil, types.AddRiskAssessmentOutput{}, fmt.Errorf("failed to create risk assessment: %w", err) + } + + return nil, types.AddRiskAssessmentOutput{ + RiskAssessment: types.NewRiskAssessment(ra), + }, nil +} + +func (r *Resolver) UpdateRiskAssessmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentUpdate) + + scope := coredata.NewScopeFromObjectID(input.ID) + + ra, err := r.riskManagement.Update(ctx, scope, riskmanagement.UpdateRiskAssessmentRequest{ + ID: input.ID, + Name: input.Name, + Description: UnwrapOmittable(input.Description), + }) + if err != nil { + return nil, types.UpdateRiskAssessmentOutput{}, fmt.Errorf("failed to update risk assessment: %w", err) + } + + return nil, types.UpdateRiskAssessmentOutput{ + RiskAssessment: types.NewRiskAssessment(ra), + }, nil +} + +func (r *Resolver) DeleteRiskAssessmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentDelete) + + scope := coredata.NewScopeFromObjectID(input.ID) + + err := r.riskManagement.Delete(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteRiskAssessmentOutput{}, fmt.Errorf("failed to delete risk assessment: %w", err) + } + + return nil, types.DeleteRiskAssessmentOutput{ + DeletedRiskAssessmentID: input.ID, + }, nil +} +func (r *Resolver) ListRiskAssessmentScopesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentScopesInput) (*mcp.CallToolResult, types.ListRiskAssessmentScopesOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentID, probo.ActionRiskAssessmentScopeList) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentID) + + pageOrderBy := page.OrderBy[coredata.RiskAssessmentScopeOrderField]{ + Field: coredata.RiskAssessmentScopeOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskAssessmentScopeOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.riskManagement.ListScopesForRiskAssessmentID(ctx, scope, input.RiskAssessmentID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list risk assessment scopes: %w", err)) + } + + return nil, types.NewListRiskAssessmentScopesOutput(p), nil +} + +func (r *Resolver) GetRiskAssessmentScopeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentScopeInput) (*mcp.CallToolResult, types.GetRiskAssessmentScopeOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScopeGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + s, err := r.riskManagement.GetScope(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentScopeOutput{}, fmt.Errorf("failed to get risk assessment scope: %w", err) + } + + return nil, types.GetRiskAssessmentScopeOutput{ + RiskAssessmentScope: types.NewRiskAssessmentScope(s), + }, nil +} + +func (r *Resolver) AddRiskAssessmentScopeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentScopeInput) (*mcp.CallToolResult, types.AddRiskAssessmentScopeOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentID, probo.ActionRiskAssessmentScopeCreate) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentID) + + s, err := r.riskManagement.CreateScope(ctx, scope, riskmanagement.CreateRiskAssessmentScopeRequest{ + RiskAssessmentID: input.RiskAssessmentID, + Name: input.Name, + }) + if err != nil { + return nil, types.AddRiskAssessmentScopeOutput{}, fmt.Errorf("failed to create risk assessment scope: %w", err) + } + + return nil, types.AddRiskAssessmentScopeOutput{ + RiskAssessmentScope: types.NewRiskAssessmentScope(s), + }, nil +} + +func (r *Resolver) UpdateRiskAssessmentScopeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentScopeInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentScopeOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScopeUpdate) + + scope := coredata.NewScopeFromObjectID(input.ID) + + s, err := r.riskManagement.UpdateScope(ctx, scope, riskmanagement.UpdateRiskAssessmentScopeRequest{ + ID: input.ID, + Name: input.Name, + }) + if err != nil { + return nil, types.UpdateRiskAssessmentScopeOutput{}, fmt.Errorf("failed to update risk assessment scope: %w", err) + } + + return nil, types.UpdateRiskAssessmentScopeOutput{ + RiskAssessmentScope: types.NewRiskAssessmentScope(s), + }, nil +} + +func (r *Resolver) DeleteRiskAssessmentScopeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentScopeInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentScopeOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScopeDelete) + + scope := coredata.NewScopeFromObjectID(input.ID) + + err := r.riskManagement.DeleteScope(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteRiskAssessmentScopeOutput{}, fmt.Errorf("failed to delete risk assessment scope: %w", err) + } + + return nil, types.DeleteRiskAssessmentScopeOutput{ + DeletedRiskAssessmentScopeID: input.ID, + }, nil +} +func (r *Resolver) ListRiskAssessmentNodesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentNodesInput) (*mcp.CallToolResult, types.ListRiskAssessmentNodesOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentNodeList) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + pageOrderBy := page.OrderBy[coredata.RiskAssessmentNodeOrderField]{ + Field: coredata.RiskAssessmentNodeOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskAssessmentNodeOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.riskManagement.ListNodesForScopeID(ctx, scope, input.RiskAssessmentScopeID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list risk assessment nodes: %w", err)) + } + + return nil, types.NewListRiskAssessmentNodesOutput(p), nil +} + +func (r *Resolver) GetRiskAssessmentNodeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentNodeInput) (*mcp.CallToolResult, types.GetRiskAssessmentNodeOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentNodeGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + n, err := r.riskManagement.GetNode(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentNodeOutput{}, fmt.Errorf("failed to get risk assessment node: %w", err) + } + + return nil, types.GetRiskAssessmentNodeOutput{ + RiskAssessmentNode: types.NewRiskAssessmentNode(n), + }, nil +} + +func (r *Resolver) AddRiskAssessmentNodeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentNodeInput) (*mcp.CallToolResult, types.AddRiskAssessmentNodeOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentNodeCreate) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + n, err := r.riskManagement.CreateNode(ctx, scope, riskmanagement.CreateRiskAssessmentNodeRequest{ + RiskAssessmentScopeID: input.RiskAssessmentScopeID, + NodeType: input.NodeType, + Name: input.Name, + }) + if err != nil { + return nil, types.AddRiskAssessmentNodeOutput{}, fmt.Errorf("failed to create risk assessment node: %w", err) + } + + return nil, types.AddRiskAssessmentNodeOutput{ + RiskAssessmentNode: types.NewRiskAssessmentNode(n), + }, nil +} + +func (r *Resolver) UpdateRiskAssessmentNodeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentNodeInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentNodeOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentNodeUpdate) + + scope := coredata.NewScopeFromObjectID(input.ID) + + n, err := r.riskManagement.UpdateNode(ctx, scope, riskmanagement.UpdateRiskAssessmentNodeRequest{ + ID: input.ID, + NodeType: input.NodeType, + Name: input.Name, + }) + if err != nil { + return nil, types.UpdateRiskAssessmentNodeOutput{}, fmt.Errorf("failed to update risk assessment node: %w", err) + } + + return nil, types.UpdateRiskAssessmentNodeOutput{ + RiskAssessmentNode: types.NewRiskAssessmentNode(n), + }, nil +} + +func (r *Resolver) DeleteRiskAssessmentNodeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentNodeInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentNodeOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentNodeDelete) + + scope := coredata.NewScopeFromObjectID(input.ID) + + err := r.riskManagement.DeleteNode(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteRiskAssessmentNodeOutput{}, fmt.Errorf("failed to delete risk assessment node: %w", err) + } + + return nil, types.DeleteRiskAssessmentNodeOutput{ + DeletedRiskAssessmentNodeID: input.ID, + }, nil +} +func (r *Resolver) ListRiskAssessmentProcessesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentProcessesInput) (*mcp.CallToolResult, types.ListRiskAssessmentProcessesOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentProcessList) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + pageOrderBy := page.OrderBy[coredata.RiskAssessmentProcessOrderField]{ + Field: coredata.RiskAssessmentProcessOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskAssessmentProcessOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.riskManagement.ListProcessesForScopeID(ctx, scope, input.RiskAssessmentScopeID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list risk assessment processes: %w", err)) + } + + return nil, types.NewListRiskAssessmentProcessesOutput(p), nil +} + +func (r *Resolver) GetRiskAssessmentProcessTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentProcessInput) (*mcp.CallToolResult, types.GetRiskAssessmentProcessOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentProcessGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + p, err := r.riskManagement.GetProcess(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentProcessOutput{}, fmt.Errorf("failed to get risk assessment process: %w", err) + } + + return nil, types.GetRiskAssessmentProcessOutput{ + RiskAssessmentProcess: types.NewRiskAssessmentProcess(p), + }, nil +} + +func (r *Resolver) AddRiskAssessmentProcessTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentProcessInput) (*mcp.CallToolResult, types.AddRiskAssessmentProcessOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentProcessCreate) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + p, err := r.riskManagement.CreateProcess(ctx, scope, riskmanagement.CreateRiskAssessmentProcessRequest{ + RiskAssessmentScopeID: input.RiskAssessmentScopeID, + SourceNodeID: input.SourceNodeID, + TargetNodeID: input.TargetNodeID, + Name: input.Name, + }) + if err != nil { + return nil, types.AddRiskAssessmentProcessOutput{}, fmt.Errorf("failed to create risk assessment process: %w", err) + } + + return nil, types.AddRiskAssessmentProcessOutput{ + RiskAssessmentProcess: types.NewRiskAssessmentProcess(p), + }, nil +} + +func (r *Resolver) UpdateRiskAssessmentProcessTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentProcessInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentProcessOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentProcessUpdate) + + scope := coredata.NewScopeFromObjectID(input.ID) + + p, err := r.riskManagement.UpdateProcess(ctx, scope, riskmanagement.UpdateRiskAssessmentProcessRequest{ + ID: input.ID, + SourceNodeID: input.SourceNodeID, + TargetNodeID: input.TargetNodeID, + Name: input.Name, + }) + if err != nil { + return nil, types.UpdateRiskAssessmentProcessOutput{}, fmt.Errorf("failed to update risk assessment process: %w", err) + } + + return nil, types.UpdateRiskAssessmentProcessOutput{ + RiskAssessmentProcess: types.NewRiskAssessmentProcess(p), + }, nil +} + +func (r *Resolver) DeleteRiskAssessmentProcessTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentProcessInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentProcessOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentProcessDelete) + + scope := coredata.NewScopeFromObjectID(input.ID) + + err := r.riskManagement.DeleteProcess(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteRiskAssessmentProcessOutput{}, fmt.Errorf("failed to delete risk assessment process: %w", err) + } + + return nil, types.DeleteRiskAssessmentProcessOutput{ + DeletedRiskAssessmentProcessID: input.ID, + }, nil +} +func (r *Resolver) ListRiskAssessmentThreatsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentThreatsInput) (*mcp.CallToolResult, types.ListRiskAssessmentThreatsOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentThreatList) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + pageOrderBy := page.OrderBy[coredata.RiskAssessmentThreatOrderField]{ + Field: coredata.RiskAssessmentThreatOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskAssessmentThreatOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.riskManagement.ListThreatsForScopeID(ctx, scope, input.RiskAssessmentScopeID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list risk assessment threats: %w", err)) + } + + return nil, types.NewListRiskAssessmentThreatsOutput(p), nil +} + +func (r *Resolver) GetRiskAssessmentThreatTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentThreatInput) (*mcp.CallToolResult, types.GetRiskAssessmentThreatOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentThreatGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + t, err := r.riskManagement.GetThreat(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentThreatOutput{}, fmt.Errorf("failed to get risk assessment threat: %w", err) + } + + return nil, types.GetRiskAssessmentThreatOutput{ + RiskAssessmentThreat: types.NewRiskAssessmentThreat(t), + }, nil +} + +func (r *Resolver) AddRiskAssessmentThreatTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentThreatInput) (*mcp.CallToolResult, types.AddRiskAssessmentThreatOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentThreatCreate) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + t, err := r.riskManagement.CreateThreat(ctx, scope, riskmanagement.CreateRiskAssessmentThreatRequest{ + RiskAssessmentScopeID: input.RiskAssessmentScopeID, + ProcessID: input.ProcessID, + Name: input.Name, + Category: input.Category, + }) + if err != nil { + return nil, types.AddRiskAssessmentThreatOutput{}, fmt.Errorf("failed to create risk assessment threat: %w", err) + } + + return nil, types.AddRiskAssessmentThreatOutput{ + RiskAssessmentThreat: types.NewRiskAssessmentThreat(t), + }, nil +} + +func (r *Resolver) UpdateRiskAssessmentThreatTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentThreatInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentThreatOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentThreatUpdate) + + scope := coredata.NewScopeFromObjectID(input.ID) + + t, err := r.riskManagement.UpdateThreat(ctx, scope, riskmanagement.UpdateRiskAssessmentThreatRequest{ + ID: input.ID, + ProcessID: input.ProcessID, + Name: input.Name, + Category: input.Category, + }) + if err != nil { + return nil, types.UpdateRiskAssessmentThreatOutput{}, fmt.Errorf("failed to update risk assessment threat: %w", err) + } + + return nil, types.UpdateRiskAssessmentThreatOutput{ + RiskAssessmentThreat: types.NewRiskAssessmentThreat(t), + }, nil +} + +func (r *Resolver) DeleteRiskAssessmentThreatTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentThreatInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentThreatOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentThreatDelete) + + scope := coredata.NewScopeFromObjectID(input.ID) + + err := r.riskManagement.DeleteThreat(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteRiskAssessmentThreatOutput{}, fmt.Errorf("failed to delete risk assessment threat: %w", err) + } + + return nil, types.DeleteRiskAssessmentThreatOutput{ + DeletedRiskAssessmentThreatID: input.ID, + }, nil +} +func (r *Resolver) ListRiskAssessmentScenariosTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentScenariosInput) (*mcp.CallToolResult, types.ListRiskAssessmentScenariosOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentScenarioList) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + pageOrderBy := page.OrderBy[coredata.RiskAssessmentScenarioOrderField]{ + Field: coredata.RiskAssessmentScenarioOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RiskAssessmentScenarioOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.riskManagement.ListScenariosForScopeID(ctx, scope, input.RiskAssessmentScopeID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list risk assessment scenarios: %w", err)) + } + + return nil, types.NewListRiskAssessmentScenariosOutput(p), nil +} + +func (r *Resolver) GetRiskAssessmentScenarioTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentScenarioInput) (*mcp.CallToolResult, types.GetRiskAssessmentScenarioOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScenarioGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + s, err := r.riskManagement.GetScenario(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentScenarioOutput{}, fmt.Errorf("failed to get risk assessment scenario: %w", err) + } + + return nil, types.GetRiskAssessmentScenarioOutput{ + RiskAssessmentScenario: types.NewRiskAssessmentScenario(s), + }, nil +} + +func (r *Resolver) AddRiskAssessmentScenarioTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentScenarioInput) (*mcp.CallToolResult, types.AddRiskAssessmentScenarioOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentScenarioCreate) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScopeID) + + s, err := r.riskManagement.CreateScenario(ctx, scope, riskmanagement.CreateRiskAssessmentScenarioRequest{ + RiskAssessmentScopeID: input.RiskAssessmentScopeID, + Name: input.Name, + Description: input.Description, + }) + if err != nil { + return nil, types.AddRiskAssessmentScenarioOutput{}, fmt.Errorf("failed to create risk assessment scenario: %w", err) + } + + return nil, types.AddRiskAssessmentScenarioOutput{ + RiskAssessmentScenario: types.NewRiskAssessmentScenario(s), + }, nil +} + +func (r *Resolver) UpdateRiskAssessmentScenarioTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentScenarioInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentScenarioOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScenarioUpdate) + + scope := coredata.NewScopeFromObjectID(input.ID) + + s, err := r.riskManagement.UpdateScenario(ctx, scope, riskmanagement.UpdateRiskAssessmentScenarioRequest{ + ID: input.ID, + Name: input.Name, + Description: UnwrapOmittable(input.Description), + }) + if err != nil { + return nil, types.UpdateRiskAssessmentScenarioOutput{}, fmt.Errorf("failed to update risk assessment scenario: %w", err) + } + + return nil, types.UpdateRiskAssessmentScenarioOutput{ + RiskAssessmentScenario: types.NewRiskAssessmentScenario(s), + }, nil +} + +func (r *Resolver) DeleteRiskAssessmentScenarioTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentScenarioInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentScenarioOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScenarioDelete) + + scope := coredata.NewScopeFromObjectID(input.ID) + + err := r.riskManagement.DeleteScenario(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteRiskAssessmentScenarioOutput{}, fmt.Errorf("failed to delete risk assessment scenario: %w", err) + } + + return nil, types.DeleteRiskAssessmentScenarioOutput{ + DeletedRiskAssessmentScenarioID: input.ID, + }, nil +} +func (r *Resolver) LinkRiskAssessmentScenarioThreatTool(ctx context.Context, req *mcp.CallToolRequest, input *types.LinkRiskAssessmentScenarioThreatInput) (*mcp.CallToolResult, types.LinkRiskAssessmentScenarioThreatOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScenarioID, probo.ActionRiskAssessmentScenarioThreatLink) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScenarioID) + + err := r.riskManagement.LinkScenarioThreat(ctx, scope, riskmanagement.LinkRiskAssessmentScenarioThreatRequest{ + RiskAssessmentScenarioID: input.RiskAssessmentScenarioID, + ThreatID: input.ThreatID, + }) + if err != nil { + return nil, types.LinkRiskAssessmentScenarioThreatOutput{}, fmt.Errorf("failed to link scenario threat: %w", err) + } + + return nil, types.LinkRiskAssessmentScenarioThreatOutput{}, nil +} + +func (r *Resolver) UnlinkRiskAssessmentScenarioThreatTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnlinkRiskAssessmentScenarioThreatInput) (*mcp.CallToolResult, types.UnlinkRiskAssessmentScenarioThreatOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScenarioID, probo.ActionRiskAssessmentScenarioThreatUnlink) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScenarioID) + + err := r.riskManagement.UnlinkScenarioThreat(ctx, scope, riskmanagement.UnlinkRiskAssessmentScenarioThreatRequest{ + RiskAssessmentScenarioID: input.RiskAssessmentScenarioID, + ThreatID: input.ThreatID, + }) + if err != nil { + return nil, types.UnlinkRiskAssessmentScenarioThreatOutput{}, fmt.Errorf("failed to unlink scenario threat: %w", err) + } + + return nil, types.UnlinkRiskAssessmentScenarioThreatOutput{}, nil +} + +func (r *Resolver) LinkRiskAssessmentScenarioRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.LinkRiskAssessmentScenarioRiskInput) (*mcp.CallToolResult, types.LinkRiskAssessmentScenarioRiskOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScenarioID, probo.ActionRiskAssessmentScenarioRiskLink) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScenarioID) + + err := r.riskManagement.LinkScenarioRisk(ctx, scope, riskmanagement.LinkRiskAssessmentScenarioRiskRequest{ + RiskAssessmentScenarioID: input.RiskAssessmentScenarioID, + RiskID: input.RiskID, + }) + if err != nil { + return nil, types.LinkRiskAssessmentScenarioRiskOutput{}, fmt.Errorf("failed to link scenario risk: %w", err) + } + + return nil, types.LinkRiskAssessmentScenarioRiskOutput{}, nil +} + +func (r *Resolver) UnlinkRiskAssessmentScenarioRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnlinkRiskAssessmentScenarioRiskInput) (*mcp.CallToolResult, types.UnlinkRiskAssessmentScenarioRiskOutput, error) { + r.MustAuthorize(ctx, input.RiskAssessmentScenarioID, probo.ActionRiskAssessmentScenarioRiskUnlink) + + scope := coredata.NewScopeFromObjectID(input.RiskAssessmentScenarioID) + + err := r.riskManagement.UnlinkScenarioRisk(ctx, scope, riskmanagement.UnlinkRiskAssessmentScenarioRiskRequest{ + RiskAssessmentScenarioID: input.RiskAssessmentScenarioID, + RiskID: input.RiskID, + }) + if err != nil { + return nil, types.UnlinkRiskAssessmentScenarioRiskOutput{}, fmt.Errorf("failed to unlink scenario risk: %w", err) + } + + return nil, types.UnlinkRiskAssessmentScenarioRiskOutput{}, nil +} + +func (r *Resolver) GetRiskAssessmentScopeMermaidChartTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentScopeMermaidChartInput) (*mcp.CallToolResult, types.GetRiskAssessmentScopeMermaidChartOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRiskAssessmentScopeGet) + + scope := coredata.NewScopeFromObjectID(input.ID) + + chart, err := r.riskManagement.BuildScopeMermaidChart(ctx, scope, input.ID) + if err != nil { + return nil, types.GetRiskAssessmentScopeMermaidChartOutput{}, fmt.Errorf("failed to build mermaid chart: %w", err) + } + + return nil, types.GetRiskAssessmentScopeMermaidChartOutput{ + MermaidChart: chart, + }, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 2527c35ec..5affd51a8 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -10706,6 +10706,1066 @@ components: items: $ref: "#/components/schemas/SCIMEvent" + RiskAssessmentNodeType: + type: string + enum: + - ENTITY + - BOUNDARY + - ASSET + - DATA + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeType + + RiskAssessmentOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentOrderField + + RiskAssessmentOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskAssessmentOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RiskAssessmentScopeOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentScopeOrderField + + RiskAssessmentScopeOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskAssessmentScopeOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RiskAssessmentNodeOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeOrderField + + RiskAssessmentNodeOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskAssessmentNodeOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RiskAssessmentProcessOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentProcessOrderField + + RiskAssessmentProcessOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskAssessmentProcessOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RiskAssessmentThreatOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentThreatOrderField + + RiskAssessmentThreatOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskAssessmentThreatOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RiskAssessmentScenarioOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentScenarioOrderField + + RiskAssessmentScenarioOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RiskAssessmentScenarioOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RiskAssessment: + type: object + required: + - id + - organization_id + - name + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + name: + type: string + description: + type: + - string + - "null" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + RiskAssessmentScope: + type: object + required: + - id + - organization_id + - risk_assessment_id + - name + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + risk_assessment_id: + $ref: "#/components/schemas/GID" + name: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + RiskAssessmentNode: + type: object + required: + - id + - organization_id + - risk_assessment_scope_id + - node_type + - name + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + node_type: + $ref: "#/components/schemas/RiskAssessmentNodeType" + name: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + RiskAssessmentProcess: + type: object + required: + - id + - organization_id + - risk_assessment_scope_id + - source_node_id + - target_node_id + - name + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + source_node_id: + $ref: "#/components/schemas/GID" + target_node_id: + $ref: "#/components/schemas/GID" + name: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + RiskAssessmentThreat: + type: object + required: + - id + - organization_id + - risk_assessment_scope_id + - process_id + - name + - category + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + process_id: + $ref: "#/components/schemas/GID" + name: + type: string + category: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + RiskAssessmentScenario: + type: object + required: + - id + - organization_id + - risk_assessment_scope_id + - name + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + name: + type: string + description: + type: + - string + - "null" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ListRiskAssessmentsInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + order_by: + $ref: "#/components/schemas/RiskAssessmentOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRiskAssessmentsOutput: + type: object + required: + - risk_assessments + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risk_assessments: + type: array + items: + $ref: "#/components/schemas/RiskAssessment" + + GetRiskAssessmentInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment ID + + GetRiskAssessmentOutput: + type: object + required: + - risk_assessment + properties: + risk_assessment: + $ref: "#/components/schemas/RiskAssessment" + + AddRiskAssessmentInput: + type: object + required: + - organization_id + - name + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + name: + type: string + description: Risk assessment name + description: + type: string + description: Risk assessment description + + AddRiskAssessmentOutput: + type: object + required: + - risk_assessment + properties: + risk_assessment: + $ref: "#/components/schemas/RiskAssessment" + + UpdateRiskAssessmentInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment ID + name: + type: string + description: Risk assessment name + description: + type: ["string", "null"] + description: Risk assessment description + go.probo.inc/mcpgen/omittable: true + + UpdateRiskAssessmentOutput: + type: object + required: + - risk_assessment + properties: + risk_assessment: + $ref: "#/components/schemas/RiskAssessment" + + DeleteRiskAssessmentInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment ID + + DeleteRiskAssessmentOutput: + type: object + required: + - deleted_risk_assessment_id + properties: + deleted_risk_assessment_id: + $ref: "#/components/schemas/GID" + description: Deleted risk assessment ID + + ListRiskAssessmentScopesInput: + type: object + required: + - risk_assessment_id + properties: + risk_assessment_id: + $ref: "#/components/schemas/GID" + description: Risk assessment ID + order_by: + $ref: "#/components/schemas/RiskAssessmentScopeOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRiskAssessmentScopesOutput: + type: object + required: + - risk_assessment_scopes + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risk_assessment_scopes: + type: array + items: + $ref: "#/components/schemas/RiskAssessmentScope" + + GetRiskAssessmentScopeInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + + GetRiskAssessmentScopeOutput: + type: object + required: + - risk_assessment_scope + properties: + risk_assessment_scope: + $ref: "#/components/schemas/RiskAssessmentScope" + + GetRiskAssessmentScopeMermaidChartInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + + GetRiskAssessmentScopeMermaidChartOutput: + type: object + required: + - mermaid_chart + properties: + mermaid_chart: + type: string + description: Mermaid chart of the scope + + AddRiskAssessmentScopeInput: + type: object + required: + - risk_assessment_id + - name + properties: + risk_assessment_id: + $ref: "#/components/schemas/GID" + description: Risk assessment ID + name: + type: string + description: Risk assessment scope name + + AddRiskAssessmentScopeOutput: + type: object + required: + - risk_assessment_scope + properties: + risk_assessment_scope: + $ref: "#/components/schemas/RiskAssessmentScope" + + UpdateRiskAssessmentScopeInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + name: + type: string + description: Risk assessment scope name + + UpdateRiskAssessmentScopeOutput: + type: object + required: + - risk_assessment_scope + properties: + risk_assessment_scope: + $ref: "#/components/schemas/RiskAssessmentScope" + + DeleteRiskAssessmentScopeInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + + DeleteRiskAssessmentScopeOutput: + type: object + required: + - deleted_risk_assessment_scope_id + properties: + deleted_risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Deleted risk assessment scope ID + + ListRiskAssessmentNodesInput: + type: object + required: + - risk_assessment_scope_id + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + order_by: + $ref: "#/components/schemas/RiskAssessmentNodeOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRiskAssessmentNodesOutput: + type: object + required: + - risk_assessment_nodes + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risk_assessment_nodes: + type: array + items: + $ref: "#/components/schemas/RiskAssessmentNode" + + GetRiskAssessmentNodeInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment node ID + + GetRiskAssessmentNodeOutput: + type: object + required: + - risk_assessment_node + properties: + risk_assessment_node: + $ref: "#/components/schemas/RiskAssessmentNode" + + AddRiskAssessmentNodeInput: + type: object + required: + - risk_assessment_scope_id + - node_type + - name + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + node_type: + $ref: "#/components/schemas/RiskAssessmentNodeType" + description: Node type + name: + type: string + description: Risk assessment node name + + AddRiskAssessmentNodeOutput: + type: object + required: + - risk_assessment_node + properties: + risk_assessment_node: + $ref: "#/components/schemas/RiskAssessmentNode" + + UpdateRiskAssessmentNodeInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment node ID + node_type: + $ref: "#/components/schemas/RiskAssessmentNodeType" + description: Node type + name: + type: string + description: Risk assessment node name + + UpdateRiskAssessmentNodeOutput: + type: object + required: + - risk_assessment_node + properties: + risk_assessment_node: + $ref: "#/components/schemas/RiskAssessmentNode" + + DeleteRiskAssessmentNodeInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment node ID + + DeleteRiskAssessmentNodeOutput: + type: object + required: + - deleted_risk_assessment_node_id + properties: + deleted_risk_assessment_node_id: + $ref: "#/components/schemas/GID" + description: Deleted risk assessment node ID + + ListRiskAssessmentProcessesInput: + type: object + required: + - risk_assessment_scope_id + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + order_by: + $ref: "#/components/schemas/RiskAssessmentProcessOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRiskAssessmentProcessesOutput: + type: object + required: + - risk_assessment_processes + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risk_assessment_processes: + type: array + items: + $ref: "#/components/schemas/RiskAssessmentProcess" + + GetRiskAssessmentProcessInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment process ID + + GetRiskAssessmentProcessOutput: + type: object + required: + - risk_assessment_process + properties: + risk_assessment_process: + $ref: "#/components/schemas/RiskAssessmentProcess" + + AddRiskAssessmentProcessInput: + type: object + required: + - risk_assessment_scope_id + - source_node_id + - target_node_id + - name + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + source_node_id: + $ref: "#/components/schemas/GID" + description: Source node ID + target_node_id: + $ref: "#/components/schemas/GID" + description: Target node ID + name: + type: string + description: Risk assessment process name + + AddRiskAssessmentProcessOutput: + type: object + required: + - risk_assessment_process + properties: + risk_assessment_process: + $ref: "#/components/schemas/RiskAssessmentProcess" + + UpdateRiskAssessmentProcessInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment process ID + source_node_id: + $ref: "#/components/schemas/GID" + description: Source node ID + target_node_id: + $ref: "#/components/schemas/GID" + description: Target node ID + name: + type: string + description: Risk assessment process name + + UpdateRiskAssessmentProcessOutput: + type: object + required: + - risk_assessment_process + properties: + risk_assessment_process: + $ref: "#/components/schemas/RiskAssessmentProcess" + + DeleteRiskAssessmentProcessInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment process ID + + DeleteRiskAssessmentProcessOutput: + type: object + required: + - deleted_risk_assessment_process_id + properties: + deleted_risk_assessment_process_id: + $ref: "#/components/schemas/GID" + description: Deleted risk assessment process ID + + ListRiskAssessmentThreatsInput: + type: object + required: + - risk_assessment_scope_id + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + order_by: + $ref: "#/components/schemas/RiskAssessmentThreatOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRiskAssessmentThreatsOutput: + type: object + required: + - risk_assessment_threats + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risk_assessment_threats: + type: array + items: + $ref: "#/components/schemas/RiskAssessmentThreat" + + GetRiskAssessmentThreatInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment threat ID + + GetRiskAssessmentThreatOutput: + type: object + required: + - risk_assessment_threat + properties: + risk_assessment_threat: + $ref: "#/components/schemas/RiskAssessmentThreat" + + AddRiskAssessmentThreatInput: + type: object + required: + - risk_assessment_scope_id + - process_id + - name + - category + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + process_id: + $ref: "#/components/schemas/GID" + description: Process ID + name: + type: string + description: Risk assessment threat name + category: + type: string + description: Risk assessment threat category + + AddRiskAssessmentThreatOutput: + type: object + required: + - risk_assessment_threat + properties: + risk_assessment_threat: + $ref: "#/components/schemas/RiskAssessmentThreat" + + UpdateRiskAssessmentThreatInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment threat ID + process_id: + $ref: "#/components/schemas/GID" + description: Process ID + name: + type: string + description: Risk assessment threat name + category: + type: string + description: Risk assessment threat category + + UpdateRiskAssessmentThreatOutput: + type: object + required: + - risk_assessment_threat + properties: + risk_assessment_threat: + $ref: "#/components/schemas/RiskAssessmentThreat" + + DeleteRiskAssessmentThreatInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment threat ID + + DeleteRiskAssessmentThreatOutput: + type: object + required: + - deleted_risk_assessment_threat_id + properties: + deleted_risk_assessment_threat_id: + $ref: "#/components/schemas/GID" + description: Deleted risk assessment threat ID + + ListRiskAssessmentScenariosInput: + type: object + required: + - risk_assessment_scope_id + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + order_by: + $ref: "#/components/schemas/RiskAssessmentScenarioOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRiskAssessmentScenariosOutput: + type: object + required: + - risk_assessment_scenarios + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + risk_assessment_scenarios: + type: array + items: + $ref: "#/components/schemas/RiskAssessmentScenario" + + GetRiskAssessmentScenarioInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + + GetRiskAssessmentScenarioOutput: + type: object + required: + - risk_assessment_scenario + properties: + risk_assessment_scenario: + $ref: "#/components/schemas/RiskAssessmentScenario" + + AddRiskAssessmentScenarioInput: + type: object + required: + - risk_assessment_scope_id + - name + properties: + risk_assessment_scope_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scope ID + name: + type: string + description: Risk assessment scenario name + description: + type: string + description: Risk assessment scenario description + + AddRiskAssessmentScenarioOutput: + type: object + required: + - risk_assessment_scenario + properties: + risk_assessment_scenario: + $ref: "#/components/schemas/RiskAssessmentScenario" + + UpdateRiskAssessmentScenarioInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + name: + type: string + description: Risk assessment scenario name + description: + type: ["string", "null"] + description: Risk assessment scenario description + go.probo.inc/mcpgen/omittable: true + + UpdateRiskAssessmentScenarioOutput: + type: object + required: + - risk_assessment_scenario + properties: + risk_assessment_scenario: + $ref: "#/components/schemas/RiskAssessmentScenario" + + DeleteRiskAssessmentScenarioInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + + DeleteRiskAssessmentScenarioOutput: + type: object + required: + - deleted_risk_assessment_scenario_id + properties: + deleted_risk_assessment_scenario_id: + $ref: "#/components/schemas/GID" + description: Deleted risk assessment scenario ID + + LinkRiskAssessmentScenarioThreatInput: + type: object + required: + - risk_assessment_scenario_id + - threat_id + properties: + risk_assessment_scenario_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + threat_id: + $ref: "#/components/schemas/GID" + description: Threat ID + + LinkRiskAssessmentScenarioThreatOutput: + type: object + + UnlinkRiskAssessmentScenarioThreatInput: + type: object + required: + - risk_assessment_scenario_id + - threat_id + properties: + risk_assessment_scenario_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + threat_id: + $ref: "#/components/schemas/GID" + description: Threat ID + + UnlinkRiskAssessmentScenarioThreatOutput: + type: object + + LinkRiskAssessmentScenarioRiskInput: + type: object + required: + - risk_assessment_scenario_id + - risk_id + properties: + risk_assessment_scenario_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + risk_id: + $ref: "#/components/schemas/GID" + description: Risk ID + + LinkRiskAssessmentScenarioRiskOutput: + type: object + + UnlinkRiskAssessmentScenarioRiskInput: + type: object + required: + - risk_assessment_scenario_id + - risk_id + properties: + risk_assessment_scenario_id: + $ref: "#/components/schemas/GID" + description: Risk assessment scenario ID + risk_id: + $ref: "#/components/schemas/GID" + description: Risk ID + + UnlinkRiskAssessmentScenarioRiskOutput: + type: object + tools: - name: listOrganizations description: List all organizations the user has access to @@ -12691,3 +13751,302 @@ tools: $ref: "#/components/schemas/ListSCIMEventsInput" outputSchema: $ref: "#/components/schemas/ListSCIMEventsOutput" + - name: listRiskAssessments + description: List all risk assessments for an organization + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRiskAssessmentsInput" + outputSchema: + $ref: "#/components/schemas/ListRiskAssessmentsOutput" + - name: getRiskAssessment + description: Get a risk assessment by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentOutput" + - name: addRiskAssessment + description: Create a new risk assessment + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskAssessmentInput" + outputSchema: + $ref: "#/components/schemas/AddRiskAssessmentOutput" + - name: updateRiskAssessment + description: Update an existing risk assessment + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentOutput" + - name: deleteRiskAssessment + description: Delete a risk assessment + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentInput" + outputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentOutput" + - name: listRiskAssessmentScopes + description: List all scopes for a risk assessment + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRiskAssessmentScopesInput" + outputSchema: + $ref: "#/components/schemas/ListRiskAssessmentScopesOutput" + - name: getRiskAssessmentScope + description: Get a risk assessment scope by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentScopeInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentScopeOutput" + - name: getRiskAssessmentScopeMermaidChart + description: Get the Mermaid chart for a risk assessment scope + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentScopeMermaidChartInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentScopeMermaidChartOutput" + - name: addRiskAssessmentScope + description: Create a new risk assessment scope + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskAssessmentScopeInput" + outputSchema: + $ref: "#/components/schemas/AddRiskAssessmentScopeOutput" + - name: updateRiskAssessmentScope + description: Update an existing risk assessment scope + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentScopeInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentScopeOutput" + - name: deleteRiskAssessmentScope + description: Delete a risk assessment scope + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentScopeInput" + outputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentScopeOutput" + - name: listRiskAssessmentNodes + description: List all nodes for a risk assessment scope + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRiskAssessmentNodesInput" + outputSchema: + $ref: "#/components/schemas/ListRiskAssessmentNodesOutput" + - name: getRiskAssessmentNode + description: Get a risk assessment node by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentNodeInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentNodeOutput" + - name: addRiskAssessmentNode + description: Create a new risk assessment node + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskAssessmentNodeInput" + outputSchema: + $ref: "#/components/schemas/AddRiskAssessmentNodeOutput" + - name: updateRiskAssessmentNode + description: Update an existing risk assessment node + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentNodeInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentNodeOutput" + - name: deleteRiskAssessmentNode + description: Delete a risk assessment node + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentNodeInput" + outputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentNodeOutput" + - name: listRiskAssessmentProcesses + description: List all processes for a risk assessment scope + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRiskAssessmentProcessesInput" + outputSchema: + $ref: "#/components/schemas/ListRiskAssessmentProcessesOutput" + - name: getRiskAssessmentProcess + description: Get a risk assessment process by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentProcessInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentProcessOutput" + - name: addRiskAssessmentProcess + description: Create a new risk assessment process + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskAssessmentProcessInput" + outputSchema: + $ref: "#/components/schemas/AddRiskAssessmentProcessOutput" + - name: updateRiskAssessmentProcess + description: Update an existing risk assessment process + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentProcessInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentProcessOutput" + - name: deleteRiskAssessmentProcess + description: Delete a risk assessment process + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentProcessInput" + outputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentProcessOutput" + - name: listRiskAssessmentThreats + description: List all threats for a risk assessment scope + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRiskAssessmentThreatsInput" + outputSchema: + $ref: "#/components/schemas/ListRiskAssessmentThreatsOutput" + - name: getRiskAssessmentThreat + description: Get a risk assessment threat by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentThreatInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentThreatOutput" + - name: addRiskAssessmentThreat + description: Create a new risk assessment threat + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskAssessmentThreatInput" + outputSchema: + $ref: "#/components/schemas/AddRiskAssessmentThreatOutput" + - name: updateRiskAssessmentThreat + description: Update an existing risk assessment threat + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentThreatInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentThreatOutput" + - name: deleteRiskAssessmentThreat + description: Delete a risk assessment threat + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentThreatInput" + outputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentThreatOutput" + - name: listRiskAssessmentScenarios + description: List all scenarios for a risk assessment scope + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRiskAssessmentScenariosInput" + outputSchema: + $ref: "#/components/schemas/ListRiskAssessmentScenariosOutput" + - name: getRiskAssessmentScenario + description: Get a risk assessment scenario by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRiskAssessmentScenarioInput" + outputSchema: + $ref: "#/components/schemas/GetRiskAssessmentScenarioOutput" + - name: addRiskAssessmentScenario + description: Create a new risk assessment scenario + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRiskAssessmentScenarioInput" + outputSchema: + $ref: "#/components/schemas/AddRiskAssessmentScenarioOutput" + - name: updateRiskAssessmentScenario + description: Update an existing risk assessment scenario + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentScenarioInput" + outputSchema: + $ref: "#/components/schemas/UpdateRiskAssessmentScenarioOutput" + - name: deleteRiskAssessmentScenario + description: Delete a risk assessment scenario + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentScenarioInput" + outputSchema: + $ref: "#/components/schemas/DeleteRiskAssessmentScenarioOutput" + - name: linkRiskAssessmentScenarioThreat + description: Link a threat to a risk assessment scenario + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/LinkRiskAssessmentScenarioThreatInput" + outputSchema: + $ref: "#/components/schemas/LinkRiskAssessmentScenarioThreatOutput" + - name: unlinkRiskAssessmentScenarioThreat + description: Unlink a threat from a risk assessment scenario + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioThreatInput" + outputSchema: + $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioThreatOutput" + - name: linkRiskAssessmentScenarioRisk + description: Link a risk to a risk assessment scenario + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/LinkRiskAssessmentScenarioRiskInput" + outputSchema: + $ref: "#/components/schemas/LinkRiskAssessmentScenarioRiskOutput" + - name: unlinkRiskAssessmentScenarioRisk + description: Unlink a risk from a risk assessment scenario + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioRiskInput" + outputSchema: + $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioRiskOutput" diff --git a/pkg/server/api/mcp/v1/types/risk_assessment.go b/pkg/server/api/mcp/v1/types/risk_assessment.go new file mode 100644 index 000000000..add4b1b1d --- /dev/null +++ b/pkg/server/api/mcp/v1/types/risk_assessment.go @@ -0,0 +1,218 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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 types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewRiskAssessment(ra *coredata.RiskAssessment) *RiskAssessment { + return &RiskAssessment{ + ID: ra.ID, + OrganizationID: ra.OrganizationID, + Name: ra.Name, + Description: ra.Description, + CreatedAt: ra.CreatedAt, + UpdatedAt: ra.UpdatedAt, + } +} + +func NewListRiskAssessmentsOutput( + p *page.Page[*coredata.RiskAssessment, coredata.RiskAssessmentOrderField], +) ListRiskAssessmentsOutput { + items := make([]*RiskAssessment, 0, len(p.Data)) + for _, v := range p.Data { + items = append(items, NewRiskAssessment(v)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRiskAssessmentsOutput{ + NextCursor: nextCursor, + RiskAssessments: items, + } +} + +func NewRiskAssessmentScope(s *coredata.RiskAssessmentScope) *RiskAssessmentScope { + return &RiskAssessmentScope{ + ID: s.ID, + OrganizationID: s.OrganizationID, + RiskAssessmentID: s.RiskAssessmentID, + Name: s.Name, + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + } +} + +func NewListRiskAssessmentScopesOutput( + p *page.Page[*coredata.RiskAssessmentScope, coredata.RiskAssessmentScopeOrderField], +) ListRiskAssessmentScopesOutput { + items := make([]*RiskAssessmentScope, 0, len(p.Data)) + for _, v := range p.Data { + items = append(items, NewRiskAssessmentScope(v)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRiskAssessmentScopesOutput{ + NextCursor: nextCursor, + RiskAssessmentScopes: items, + } +} + +func NewRiskAssessmentNode(n *coredata.RiskAssessmentNode) *RiskAssessmentNode { + return &RiskAssessmentNode{ + ID: n.ID, + OrganizationID: n.OrganizationID, + RiskAssessmentScopeID: n.RiskAssessmentScopeID, + NodeType: n.NodeType, + Name: n.Name, + CreatedAt: n.CreatedAt, + UpdatedAt: n.UpdatedAt, + } +} + +func NewListRiskAssessmentNodesOutput( + p *page.Page[*coredata.RiskAssessmentNode, coredata.RiskAssessmentNodeOrderField], +) ListRiskAssessmentNodesOutput { + items := make([]*RiskAssessmentNode, 0, len(p.Data)) + for _, v := range p.Data { + items = append(items, NewRiskAssessmentNode(v)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRiskAssessmentNodesOutput{ + NextCursor: nextCursor, + RiskAssessmentNodes: items, + } +} + +func NewRiskAssessmentProcess(p *coredata.RiskAssessmentProcess) *RiskAssessmentProcess { + return &RiskAssessmentProcess{ + ID: p.ID, + OrganizationID: p.OrganizationID, + RiskAssessmentScopeID: p.RiskAssessmentScopeID, + SourceNodeID: p.SourceNodeID, + TargetNodeID: p.TargetNodeID, + Name: p.Name, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + } +} + +func NewListRiskAssessmentProcessesOutput( + p *page.Page[*coredata.RiskAssessmentProcess, coredata.RiskAssessmentProcessOrderField], +) ListRiskAssessmentProcessesOutput { + items := make([]*RiskAssessmentProcess, 0, len(p.Data)) + for _, v := range p.Data { + items = append(items, NewRiskAssessmentProcess(v)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRiskAssessmentProcessesOutput{ + NextCursor: nextCursor, + RiskAssessmentProcesses: items, + } +} + +func NewRiskAssessmentThreat(t *coredata.RiskAssessmentThreat) *RiskAssessmentThreat { + return &RiskAssessmentThreat{ + ID: t.ID, + OrganizationID: t.OrganizationID, + RiskAssessmentScopeID: t.RiskAssessmentScopeID, + ProcessID: t.ProcessID, + Name: t.Name, + Category: t.Category, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } +} + +func NewListRiskAssessmentThreatsOutput( + p *page.Page[*coredata.RiskAssessmentThreat, coredata.RiskAssessmentThreatOrderField], +) ListRiskAssessmentThreatsOutput { + items := make([]*RiskAssessmentThreat, 0, len(p.Data)) + for _, v := range p.Data { + items = append(items, NewRiskAssessmentThreat(v)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRiskAssessmentThreatsOutput{ + NextCursor: nextCursor, + RiskAssessmentThreats: items, + } +} + +func NewRiskAssessmentScenario(s *coredata.RiskAssessmentScenario) *RiskAssessmentScenario { + return &RiskAssessmentScenario{ + ID: s.ID, + OrganizationID: s.OrganizationID, + RiskAssessmentScopeID: s.RiskAssessmentScopeID, + Name: s.Name, + Description: s.Description, + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + } +} + +func NewListRiskAssessmentScenariosOutput( + p *page.Page[*coredata.RiskAssessmentScenario, coredata.RiskAssessmentScenarioOrderField], +) ListRiskAssessmentScenariosOutput { + items := make([]*RiskAssessmentScenario, 0, len(p.Data)) + for _, v := range p.Data { + items = append(items, NewRiskAssessmentScenario(v)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRiskAssessmentScenariosOutput{ + NextCursor: nextCursor, + RiskAssessmentScenarios: items, + } +} diff --git a/pkg/server/api/mcp/v1/v1_handler.go b/pkg/server/api/mcp/v1/v1_handler.go index ad0d99976..8d15b5071 100644 --- a/pkg/server/api/mcp/v1/v1_handler.go +++ b/pkg/server/api/mcp/v1/v1_handler.go @@ -25,22 +25,24 @@ import ( "go.probo.inc/probo/pkg/cookiebanner" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/probo" + "go.probo.inc/probo/pkg/riskmanagement" "go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/mcp/mcputils" "go.probo.inc/probo/pkg/server/api/mcp/v1/server" ) -func NewMux(logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service, accessReviewSvc *accessreview.Service, cookieBannerSvc *cookiebanner.Service, tokenSecret string) *chi.Mux { +func NewMux(logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service, accessReviewSvc *accessreview.Service, cookieBannerSvc *cookiebanner.Service, riskManagementSvc *riskmanagement.Service, tokenSecret string) *chi.Mux { logger = logger.Named("mcp.v1") logger.Info("initializing MCP server") resolver := &Resolver{ - proboSvc: proboSvc, - iamSvc: iamSvc, - accessReview: accessReviewSvc, - cookieBanner: cookieBannerSvc, - logger: logger, + proboSvc: proboSvc, + iamSvc: iamSvc, + accessReview: accessReviewSvc, + cookieBanner: cookieBannerSvc, + riskManagement: riskManagementSvc, + logger: logger, } mcpServer := server.New(resolver, mcpgenmcp.WithRecoverFunc(mcputils.NewRecoverFunc(logger)))