Add risk assessment boundary model

Introduce RiskAssessmentBoundary as a first-class, self-nesting entity that
groups nodes within a risk assessment scope, and thread it through every
surface.

- coredata: new risk_assessment_boundaries table + migration, boundary_id on
  nodes, self-referential parent_boundary_id, entity type registration
- riskmanagement: boundary CRUD service methods, boundary_id wiring on node
  create/update, scope-membership and self-parent validation, nested-subgraph
  Mermaid rendering
- IAM: core:risk-assessment-boundary:{get,list,create,update,delete} actions
  and viewer/auditor read policies
- console GraphQL: RiskAssessmentBoundary type, connection, order enum, CRUD
  mutations, boundaries field on scope, boundaryId on nodes
- CLI: risk-assessment boundary command group and --boundary-id on nodes
- MCP: boundary tools and boundary_id on node tools
- n8n: boundary operations and boundary fields on node operations
- console UI: boundary list/create/edit, boundary selector on nodes, diagram
  refetch on boundary changes

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-06-08 15:37:56 +02:00
parent b643c8eb6d
commit dbf915047d
44 changed files with 3620 additions and 79 deletions

View File

@@ -0,0 +1,96 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
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: ['createBoundary'],
},
},
default: '',
description: 'The ID of the scope',
required: true,
},
{
displayName: 'Name',
name: 'name',
type: 'string',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['createBoundary'],
},
},
default: '',
description: 'The name of the boundary',
required: true,
},
{
displayName: 'Parent Boundary ID',
name: 'parentBoundaryId',
type: 'string',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['createBoundary'],
},
},
default: '',
description: 'The ID of the parent boundary, for nested boundaries (optional)',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string;
const name = this.getNodeParameter('name', itemIndex) as string;
const parentBoundaryId = this.getNodeParameter('parentBoundaryId', itemIndex, '') as string;
const query = `
mutation CreateRiskAssessmentBoundary($input: CreateRiskAssessmentBoundaryInput!) {
createRiskAssessmentBoundary(input: $input) {
riskAssessmentBoundaryEdge {
node {
id
riskAssessmentScopeId
parentBoundaryId
name
createdAt
updatedAt
}
}
}
}
`;
const input: Record<string, unknown> = { riskAssessmentScopeId, name };
if (parentBoundaryId) input.parentBoundaryId = parentBoundaryId;
const responseData = await proboApiRequest.call(this, query, { input });
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -45,10 +45,6 @@ export const description: INodeProperties[] = [
name: 'Entity',
value: 'ENTITY',
},
{
name: 'Boundary',
value: 'BOUNDARY',
},
{
name: 'Asset',
value: 'ASSET',
@@ -76,6 +72,19 @@ export const description: INodeProperties[] = [
description: 'The name of the node',
required: true,
},
{
displayName: 'Boundary ID',
name: 'boundaryId',
type: 'string',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['createNode'],
},
},
default: '',
description: 'The ID of the boundary that contains this node (optional)',
},
];
export async function execute(
@@ -85,6 +94,7 @@ export async function execute(
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 boundaryId = this.getNodeParameter('boundaryId', itemIndex, '') as string;
const query = `
mutation CreateRiskAssessmentNode($input: CreateRiskAssessmentNodeInput!) {
@@ -93,6 +103,7 @@ export async function execute(
node {
id
riskAssessmentScopeId
boundaryId
nodeType
name
createdAt
@@ -103,9 +114,10 @@ export async function execute(
}
`;
const responseData = await proboApiRequest.call(this, query, {
input: { riskAssessmentScopeId, nodeType, name },
});
const input: Record<string, unknown> = { riskAssessmentScopeId, nodeType, name };
if (boundaryId) input.boundaryId = boundaryId;
const responseData = await proboApiRequest.call(this, query, { input });
return {
json: responseData,

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Boundary ID',
name: 'boundaryId',
type: 'string',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['deleteBoundary'],
},
},
default: '',
description: 'The ID of the boundary to delete',
required: true,
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const boundaryId = this.getNodeParameter('boundaryId', itemIndex) as string;
const query = `
mutation DeleteRiskAssessmentBoundary($input: DeleteRiskAssessmentBoundaryInput!) {
deleteRiskAssessmentBoundary(input: $input) {
deletedRiskAssessmentBoundaryId
}
}
`;
const responseData = await proboApiRequest.call(this, query, {
input: { riskAssessmentBoundaryId: boundaryId },
});
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -0,0 +1,115 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
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: ['getAllBoundaries'],
},
},
default: '',
description: 'The ID of the scope',
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['getAllBoundaries'],
},
},
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: ['getAllBoundaries'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
},
default: 50,
description: 'Max number of results to return',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
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 GetBoundaries($scopeId: ID!, $first: Int, $after: CursorKey) {
node(id: $scopeId) {
... on RiskAssessmentScope {
boundaries(first: $first, after: $after) {
edges {
node {
id
riskAssessmentScopeId
parentBoundaryId
name
createdAt
updatedAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;
const boundaries = await proboApiRequestAllItems.call(
this,
query,
{ scopeId },
(response) => {
const data = response?.data as IDataObject | undefined;
const node = data?.node as IDataObject | undefined;
return node?.boundaries as IDataObject | undefined;
},
returnAll,
limit,
);
return {
json: { boundaries },
pairedItem: { item: itemIndex },
};
}

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Boundary ID',
name: 'boundaryId',
type: 'string',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['getBoundary'],
},
},
default: '',
description: 'The ID of the boundary',
required: true,
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const boundaryId = this.getNodeParameter('boundaryId', itemIndex) as string;
const query = `
query GetRiskAssessmentBoundary($id: ID!) {
node(id: $id) {
... on RiskAssessmentBoundary {
id
riskAssessmentScopeId
parentBoundaryId
name
createdAt
updatedAt
}
}
}
`;
const responseData = await proboApiRequest.call(this, query, { id: boundaryId });
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}

View File

@@ -29,6 +29,11 @@ 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 createBoundaryOp from './createBoundary.operation';
import * as getBoundaryOp from './getBoundary.operation';
import * as getAllBoundariesOp from './getAllBoundaries.operation';
import * as updateBoundaryOp from './updateBoundary.operation';
import * as deleteBoundaryOp from './deleteBoundary.operation';
import * as createProcessOp from './createProcess.operation';
import * as getProcessOp from './getProcess.operation';
import * as getAllProcessesOp from './getAllProcesses.operation';
@@ -67,6 +72,12 @@ export const description: INodeProperties[] = [
description: 'Create a risk assessment',
action: 'Create a risk assessment',
},
{
name: 'Create Boundary',
value: 'createBoundary',
description: 'Create a boundary in a scope',
action: 'Create a boundary',
},
{
name: 'Create Node',
value: 'createNode',
@@ -103,6 +114,12 @@ export const description: INodeProperties[] = [
description: 'Delete a risk assessment',
action: 'Delete a risk assessment',
},
{
name: 'Delete Boundary',
value: 'deleteBoundary',
description: 'Delete a boundary',
action: 'Delete a boundary',
},
{
name: 'Delete Node',
value: 'deleteNode',
@@ -139,12 +156,23 @@ export const description: INodeProperties[] = [
description: 'Get a risk assessment',
action: 'Get a risk assessment',
},
{
name: 'Get Boundary',
value: 'getBoundary',
description: 'Get a boundary',
action: 'Get a boundary',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many risk assessments',
action: 'Get many risk assessments',
},
{
name: 'Get Many Boundaries',
value: 'getAllBoundaries',
action: 'Get many boundaries',
},
{
name: 'Get Many Nodes',
value: 'getAllNodes',
@@ -236,6 +264,12 @@ export const description: INodeProperties[] = [
description: 'Update a risk assessment',
action: 'Update a risk assessment',
},
{
name: 'Update Boundary',
value: 'updateBoundary',
description: 'Update a boundary',
action: 'Update a boundary',
},
{
name: 'Update Node',
value: 'updateNode',
@@ -285,6 +319,11 @@ export const description: INodeProperties[] = [
...getAllNodesOp.description,
...updateNodeOp.description,
...deleteNodeOp.description,
...createBoundaryOp.description,
...getBoundaryOp.description,
...getAllBoundariesOp.description,
...updateBoundaryOp.description,
...deleteBoundaryOp.description,
...createProcessOp.description,
...getProcessOp.description,
...getAllProcessesOp.description,
@@ -323,6 +362,11 @@ export {
getAllNodesOp as getAllNodes,
updateNodeOp as updateNode,
deleteNodeOp as deleteNode,
createBoundaryOp as createBoundary,
getBoundaryOp as getBoundary,
getAllBoundariesOp as getAllBoundaries,
updateBoundaryOp as updateBoundary,
deleteBoundaryOp as deleteBoundary,
createProcessOp as createProcess,
getProcessOp as getProcess,
getAllProcessesOp as getAllProcesses,

View File

@@ -0,0 +1,105 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { proboApiRequest } from '../../GenericFunctions';
export const description: INodeProperties[] = [
{
displayName: 'Boundary ID',
name: 'boundaryId',
type: 'string',
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['updateBoundary'],
},
},
default: '',
description: 'The ID of the boundary to update',
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['riskAssessment'],
operation: ['updateBoundary'],
},
},
options: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the boundary',
},
{
displayName: 'Parent Boundary ID',
name: 'parentBoundaryId',
type: 'string',
default: '',
description: 'The ID of the parent boundary. Leave empty to make the boundary top-level.',
},
],
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const boundaryId = this.getNodeParameter('boundaryId', itemIndex) as string;
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
name?: string;
parentBoundaryId?: string;
};
const query = `
mutation UpdateRiskAssessmentBoundary($input: UpdateRiskAssessmentBoundaryInput!) {
updateRiskAssessmentBoundary(input: $input) {
riskAssessmentBoundary {
id
riskAssessmentScopeId
parentBoundaryId
name
createdAt
updatedAt
}
}
}
`;
const input: Record<string, unknown> = { id: boundaryId };
if (additionalFields.name) input.name = additionalFields.name;
if (additionalFields.parentBoundaryId !== undefined) {
input.parentBoundaryId = additionalFields.parentBoundaryId || null;
}
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 },
};
}

View File

@@ -59,10 +59,6 @@ export const description: INodeProperties[] = [
name: 'Entity',
value: 'ENTITY',
},
{
name: 'Boundary',
value: 'BOUNDARY',
},
{
name: 'Asset',
value: 'ASSET',
@@ -75,6 +71,13 @@ export const description: INodeProperties[] = [
default: 'ENTITY',
description: 'The type of the node',
},
{
displayName: 'Boundary ID',
name: 'boundaryId',
type: 'string',
default: '',
description: 'The ID of the boundary that contains this node. Leave empty to move it to the top level.',
},
],
},
];
@@ -87,6 +90,7 @@ export async function execute(
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
name?: string;
nodeType?: string;
boundaryId?: string;
};
const query = `
@@ -95,6 +99,7 @@ export async function execute(
riskAssessmentNode {
id
riskAssessmentScopeId
boundaryId
nodeType
name
createdAt
@@ -107,6 +112,9 @@ export async function execute(
const input: Record<string, unknown> = { id: nodeId };
if (additionalFields.name) input.name = additionalFields.name;
if (additionalFields.nodeType) input.nodeType = additionalFields.nodeType;
if (additionalFields.boundaryId !== undefined) {
input.boundaryId = additionalFields.boundaryId || null;
}
if (Object.keys(input).length === 1) {
throw new Error('At least one field must be provided to update');