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:
@@ -0,0 +1,120 @@
|
|||||||
|
// 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 { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
ActionDropdown,
|
||||||
|
Breadcrumb,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DropdownItem,
|
||||||
|
Field,
|
||||||
|
IconPencil,
|
||||||
|
IconTrashCan,
|
||||||
|
Option,
|
||||||
|
useConfirm,
|
||||||
|
useDialogRef,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { graphql, useMutation } from "react-relay";
|
||||||
|
|
||||||
|
import type { BoundaryActionsDeleteMutation } from "#/__generated__/core/BoundaryActionsDeleteMutation.graphql";
|
||||||
|
import type { BoundaryActionsUpdateMutation } from "#/__generated__/core/BoundaryActionsUpdateMutation.graphql";
|
||||||
|
import { ControlledField } from "#/components/form/ControlledField";
|
||||||
|
|
||||||
|
const updateBoundaryMutation = graphql`
|
||||||
|
mutation BoundaryActionsUpdateMutation($input: UpdateRiskAssessmentBoundaryInput!) {
|
||||||
|
updateRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundary { id name parentBoundaryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const deleteBoundaryMutation = graphql`
|
||||||
|
mutation BoundaryActionsDeleteMutation(
|
||||||
|
$input: DeleteRiskAssessmentBoundaryInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
deleteRiskAssessmentBoundary(input: $input) {
|
||||||
|
deletedRiskAssessmentBoundaryId @deleteEdge(connections: $connections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function BoundaryActions(props: {
|
||||||
|
boundary: { id: string; name: string; parentBoundaryId: string | null };
|
||||||
|
boundaries: { id: string; name: string }[];
|
||||||
|
connectionId: string;
|
||||||
|
}) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
const [updateBoundary] = useMutation<BoundaryActionsUpdateMutation>(updateBoundaryMutation);
|
||||||
|
const [deleteBoundary] = useMutation<BoundaryActionsDeleteMutation>(deleteBoundaryMutation);
|
||||||
|
const { register, control, handleSubmit } = useForm({
|
||||||
|
values: {
|
||||||
|
name: props.boundary.name,
|
||||||
|
parentBoundaryId: props.boundary.parentBoundaryId ?? "none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const parentOptions = props.boundaries.filter(b => b.id !== props.boundary.id);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionDropdown>
|
||||||
|
<DropdownItem icon={IconPencil} onSelect={() => dialogRef.current?.open()}>
|
||||||
|
{__("Edit")}
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
icon={IconTrashCan}
|
||||||
|
variant="danger"
|
||||||
|
onSelect={() => confirm(
|
||||||
|
() => {
|
||||||
|
deleteBoundary({
|
||||||
|
variables: {
|
||||||
|
input: { riskAssessmentBoundaryId: props.boundary.id },
|
||||||
|
connections: [props.connectionId],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ message: __("Delete this boundary? Nodes and nested boundaries inside it will be moved to the top level.") },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{__("Delete")}
|
||||||
|
</DropdownItem>
|
||||||
|
</ActionDropdown>
|
||||||
|
<Dialog className="max-w-lg" ref={dialogRef} title={<Breadcrumb items={[__("Boundaries"), __("Edit")]} />}>
|
||||||
|
<form onSubmit={e => void handleSubmit((d) => {
|
||||||
|
updateBoundary({
|
||||||
|
variables: { input: { id: props.boundary.id, name: d.name, parentBoundaryId: d.parentBoundaryId === "none" ? null : d.parentBoundaryId } },
|
||||||
|
onCompleted: () => { dialogRef.current?.close(); },
|
||||||
|
});
|
||||||
|
})(e)}
|
||||||
|
>
|
||||||
|
<DialogContent padded className="space-y-4">
|
||||||
|
<Field label={__("Name")} {...register("name", { required: __("This field is required") })} type="text" />
|
||||||
|
<ControlledField label={__("Parent boundary")} name="parentBoundaryId" control={control} type="select">
|
||||||
|
<Option value="none">{__("None (top level)")}</Option>
|
||||||
|
{parentOptions.map(b => (
|
||||||
|
<Option key={b.id} value={b.id}>{b.name}</Option>
|
||||||
|
))}
|
||||||
|
</ControlledField>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter><Button type="submit">{__("Save")}</Button></DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// 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 { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
Field,
|
||||||
|
IconPlusLarge,
|
||||||
|
Option,
|
||||||
|
useDialogRef,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { graphql, useMutation } from "react-relay";
|
||||||
|
|
||||||
|
import type { CreateBoundaryDialogMutation } from "#/__generated__/core/CreateBoundaryDialogMutation.graphql";
|
||||||
|
import { ControlledField } from "#/components/form/ControlledField";
|
||||||
|
|
||||||
|
const createBoundaryMutation = graphql`
|
||||||
|
mutation CreateBoundaryDialogMutation(
|
||||||
|
$input: CreateRiskAssessmentBoundaryInput!
|
||||||
|
$connections: [ID!]!
|
||||||
|
) {
|
||||||
|
createRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundaryEdge @appendEdge(connections: $connections) {
|
||||||
|
node { id name parentBoundaryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function CreateBoundaryDialog(props: {
|
||||||
|
scopeId: string;
|
||||||
|
connectionId: string;
|
||||||
|
boundaries: { id: string; name: string }[];
|
||||||
|
}) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const dialogRef = useDialogRef();
|
||||||
|
const [createBoundary, isCreating] = useMutation<CreateBoundaryDialogMutation>(createBoundaryMutation);
|
||||||
|
const { register, control, handleSubmit, reset, formState } = useForm({
|
||||||
|
defaultValues: { name: "", parentBoundaryId: "none" },
|
||||||
|
});
|
||||||
|
const onSubmit = (data: { name: string; parentBoundaryId: string }) => {
|
||||||
|
createBoundary({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
riskAssessmentScopeId: props.scopeId,
|
||||||
|
name: data.name,
|
||||||
|
parentBoundaryId: data.parentBoundaryId === "none" ? null : data.parentBoundaryId,
|
||||||
|
},
|
||||||
|
connections: [props.connectionId],
|
||||||
|
},
|
||||||
|
onCompleted: () => {
|
||||||
|
reset();
|
||||||
|
dialogRef.current?.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className="max-w-lg"
|
||||||
|
ref={dialogRef}
|
||||||
|
trigger={<Button icon={IconPlusLarge} variant="secondary">{__("Add")}</Button>}
|
||||||
|
title={<Breadcrumb items={[__("Boundaries"), __("Add Boundary")]} />}
|
||||||
|
>
|
||||||
|
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
|
||||||
|
<DialogContent padded className="space-y-4">
|
||||||
|
<Field label={__("Name")} {...register("name", { required: __("This field is required") })} type="text" error={formState.errors.name?.message} />
|
||||||
|
<ControlledField label={__("Parent boundary")} name="parentBoundaryId" control={control} type="select">
|
||||||
|
<Option value="none">{__("None (top level)")}</Option>
|
||||||
|
{props.boundaries.map(b => (
|
||||||
|
<Option key={b.id} value={b.id}>{b.name}</Option>
|
||||||
|
))}
|
||||||
|
</ControlledField>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogFooter><Button type="submit" disabled={isCreating}>{__("Add")}</Button></DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,26 +37,31 @@ const createNodeMutation = graphql`
|
|||||||
) {
|
) {
|
||||||
createRiskAssessmentNode(input: $input) {
|
createRiskAssessmentNode(input: $input) {
|
||||||
riskAssessmentNodeEdge @appendEdge(connections: $connections) {
|
riskAssessmentNodeEdge @appendEdge(connections: $connections) {
|
||||||
node { id nodeType name }
|
node { id nodeType name boundaryId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function CreateNodeDialog(props: { scopeId: string; connectionId: string }) {
|
export function CreateNodeDialog(props: {
|
||||||
|
scopeId: string;
|
||||||
|
connectionId: string;
|
||||||
|
boundaries: { id: string; name: string }[];
|
||||||
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const dialogRef = useDialogRef();
|
const dialogRef = useDialogRef();
|
||||||
const [createNode, isCreating] = useMutation<CreateNodeDialogMutation>(createNodeMutation);
|
const [createNode, isCreating] = useMutation<CreateNodeDialogMutation>(createNodeMutation);
|
||||||
const { register, control, handleSubmit, reset, formState } = useForm({
|
const { register, control, handleSubmit, reset, formState } = useForm({
|
||||||
defaultValues: { name: "", nodeType: "ASSET" },
|
defaultValues: { name: "", nodeType: "ASSET", boundaryId: "none" },
|
||||||
});
|
});
|
||||||
const onSubmit = (data: { name: string; nodeType: string }) => {
|
const onSubmit = (data: { name: string; nodeType: string; boundaryId: string }) => {
|
||||||
createNode({
|
createNode({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
riskAssessmentScopeId: props.scopeId,
|
riskAssessmentScopeId: props.scopeId,
|
||||||
nodeType: data.nodeType as "ENTITY" | "BOUNDARY" | "ASSET" | "DATA",
|
nodeType: data.nodeType as "ENTITY" | "ASSET" | "DATA",
|
||||||
name: data.name,
|
name: data.name,
|
||||||
|
boundaryId: data.boundaryId === "none" ? null : data.boundaryId,
|
||||||
},
|
},
|
||||||
connections: [props.connectionId],
|
connections: [props.connectionId],
|
||||||
},
|
},
|
||||||
@@ -77,11 +82,18 @@ export function CreateNodeDialog(props: { scopeId: string; connectionId: string
|
|||||||
<DialogContent padded className="space-y-4">
|
<DialogContent padded className="space-y-4">
|
||||||
<ControlledField label={__("Type")} name="nodeType" control={control} type="select">
|
<ControlledField label={__("Type")} name="nodeType" control={control} type="select">
|
||||||
<Option value="ENTITY">{__("Entity")}</Option>
|
<Option value="ENTITY">{__("Entity")}</Option>
|
||||||
<Option value="BOUNDARY">{__("Boundary")}</Option>
|
|
||||||
<Option value="ASSET">{__("Asset")}</Option>
|
<Option value="ASSET">{__("Asset")}</Option>
|
||||||
<Option value="DATA">{__("Data")}</Option>
|
<Option value="DATA">{__("Data")}</Option>
|
||||||
</ControlledField>
|
</ControlledField>
|
||||||
<Field label={__("Name")} {...register("name", { required: __("This field is required") })} type="text" error={formState.errors.name?.message} />
|
<Field label={__("Name")} {...register("name", { required: __("This field is required") })} type="text" error={formState.errors.name?.message} />
|
||||||
|
{props.boundaries.length > 0 && (
|
||||||
|
<ControlledField label={__("Boundary")} name="boundaryId" control={control} type="select">
|
||||||
|
<Option value="none">{__("None")}</Option>
|
||||||
|
{props.boundaries.map(b => (
|
||||||
|
<Option key={b.id} value={b.id}>{b.name}</Option>
|
||||||
|
))}
|
||||||
|
</ControlledField>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter><Button type="submit" disabled={isCreating}>{__("Add")}</Button></DialogFooter>
|
<DialogFooter><Button type="submit" disabled={isCreating}>{__("Add")}</Button></DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { ControlledField } from "#/components/form/ControlledField";
|
|||||||
const updateNodeMutation = graphql`
|
const updateNodeMutation = graphql`
|
||||||
mutation NodeActionsUpdateMutation($input: UpdateRiskAssessmentNodeInput!) {
|
mutation NodeActionsUpdateMutation($input: UpdateRiskAssessmentNodeInput!) {
|
||||||
updateRiskAssessmentNode(input: $input) {
|
updateRiskAssessmentNode(input: $input) {
|
||||||
riskAssessmentNode { id nodeType name }
|
riskAssessmentNode { id nodeType name boundaryId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -55,7 +55,8 @@ const deleteNodeMutation = graphql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function NodeActions(props: {
|
export function NodeActions(props: {
|
||||||
node: { id: string; name: string; nodeType: string };
|
node: { id: string; name: string; nodeType: string; boundaryId: string | null };
|
||||||
|
boundaries: { id: string; name: string }[];
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
}) {
|
}) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
@@ -64,7 +65,11 @@ export function NodeActions(props: {
|
|||||||
const [updateNode] = useMutation<NodeActionsUpdateMutation>(updateNodeMutation);
|
const [updateNode] = useMutation<NodeActionsUpdateMutation>(updateNodeMutation);
|
||||||
const [deleteNode] = useMutation<NodeActionsDeleteMutation>(deleteNodeMutation);
|
const [deleteNode] = useMutation<NodeActionsDeleteMutation>(deleteNodeMutation);
|
||||||
const { register, control, handleSubmit } = useForm({
|
const { register, control, handleSubmit } = useForm({
|
||||||
values: { name: props.node.name, nodeType: props.node.nodeType },
|
values: {
|
||||||
|
name: props.node.name,
|
||||||
|
nodeType: props.node.nodeType,
|
||||||
|
boundaryId: props.node.boundaryId ?? "none",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -93,7 +98,7 @@ export function NodeActions(props: {
|
|||||||
<Dialog className="max-w-lg" ref={dialogRef} title={<Breadcrumb items={[__("Nodes"), __("Edit")]} />}>
|
<Dialog className="max-w-lg" ref={dialogRef} title={<Breadcrumb items={[__("Nodes"), __("Edit")]} />}>
|
||||||
<form onSubmit={e => void handleSubmit((d) => {
|
<form onSubmit={e => void handleSubmit((d) => {
|
||||||
updateNode({
|
updateNode({
|
||||||
variables: { input: { id: props.node.id, name: d.name, nodeType: d.nodeType as "ENTITY" | "BOUNDARY" | "ASSET" | "DATA" } },
|
variables: { input: { id: props.node.id, name: d.name, nodeType: d.nodeType as "ENTITY" | "ASSET" | "DATA", boundaryId: d.boundaryId === "none" ? null : d.boundaryId } },
|
||||||
onCompleted: () => { dialogRef.current?.close(); },
|
onCompleted: () => { dialogRef.current?.close(); },
|
||||||
});
|
});
|
||||||
})(e)}
|
})(e)}
|
||||||
@@ -101,11 +106,16 @@ export function NodeActions(props: {
|
|||||||
<DialogContent padded className="space-y-4">
|
<DialogContent padded className="space-y-4">
|
||||||
<ControlledField label={__("Type")} name="nodeType" control={control} type="select">
|
<ControlledField label={__("Type")} name="nodeType" control={control} type="select">
|
||||||
<Option value="ENTITY">{__("Entity")}</Option>
|
<Option value="ENTITY">{__("Entity")}</Option>
|
||||||
<Option value="BOUNDARY">{__("Boundary")}</Option>
|
|
||||||
<Option value="ASSET">{__("Asset")}</Option>
|
<Option value="ASSET">{__("Asset")}</Option>
|
||||||
<Option value="DATA">{__("Data")}</Option>
|
<Option value="DATA">{__("Data")}</Option>
|
||||||
</ControlledField>
|
</ControlledField>
|
||||||
<Field label={__("Name")} {...register("name", { required: __("This field is required") })} type="text" />
|
<Field label={__("Name")} {...register("name", { required: __("This field is required") })} type="text" />
|
||||||
|
<ControlledField label={__("Boundary")} name="boundaryId" control={control} type="select">
|
||||||
|
<Option value="none">{__("None")}</Option>
|
||||||
|
{props.boundaries.map(b => (
|
||||||
|
<Option key={b.id} value={b.id}>{b.name}</Option>
|
||||||
|
))}
|
||||||
|
</ControlledField>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogFooter><Button type="submit">{__("Save")}</Button></DialogFooter>
|
<DialogFooter><Button type="submit">{__("Save")}</Button></DialogFooter>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import { Link } from "react-router";
|
|||||||
import type { ScopeCardFragment$key } from "#/__generated__/core/ScopeCardFragment.graphql";
|
import type { ScopeCardFragment$key } from "#/__generated__/core/ScopeCardFragment.graphql";
|
||||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||||
|
|
||||||
|
import { BoundaryActions } from "./BoundaryActions";
|
||||||
|
import { CreateBoundaryDialog } from "./CreateBoundaryDialog";
|
||||||
import { CreateNodeDialog } from "./CreateNodeDialog";
|
import { CreateNodeDialog } from "./CreateNodeDialog";
|
||||||
import { CreateProcessDialog } from "./CreateProcessDialog";
|
import { CreateProcessDialog } from "./CreateProcessDialog";
|
||||||
import { CreateScenarioInScopeDialog } from "./CreateScenarioInScopeDialog";
|
import { CreateScenarioInScopeDialog } from "./CreateScenarioInScopeDialog";
|
||||||
@@ -51,7 +53,14 @@ export const scopeCardFragment = graphql`
|
|||||||
@connection(key: "RiskAssessmentScope_nodes", filters: []) {
|
@connection(key: "RiskAssessmentScope_nodes", filters: []) {
|
||||||
__id
|
__id
|
||||||
edges {
|
edges {
|
||||||
node { id nodeType name }
|
node { id nodeType name boundaryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boundaries(first: 100)
|
||||||
|
@connection(key: "RiskAssessmentScope_boundaries", filters: []) {
|
||||||
|
__id
|
||||||
|
edges {
|
||||||
|
node { id name parentBoundaryId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
processes(first: 100)
|
processes(first: 100)
|
||||||
@@ -112,11 +121,15 @@ export function ScopeCard(props: {
|
|||||||
const { scopesConnectionId } = props;
|
const { scopesConnectionId } = props;
|
||||||
|
|
||||||
const nodes = scope.nodes?.edges.map(e => e.node) ?? [];
|
const nodes = scope.nodes?.edges.map(e => e.node) ?? [];
|
||||||
|
const boundaries = scope.boundaries?.edges.map(e => e.node) ?? [];
|
||||||
const processes = scope.processes?.edges.map(e => e.node) ?? [];
|
const processes = scope.processes?.edges.map(e => e.node) ?? [];
|
||||||
const threats = scope.threats?.edges.map(e => e.node) ?? [];
|
const threats = scope.threats?.edges.map(e => e.node) ?? [];
|
||||||
const scenarios = scope.scenarios?.edges.map(e => e.node) ?? [];
|
const scenarios = scope.scenarios?.edges.map(e => e.node) ?? [];
|
||||||
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
||||||
|
const boundaryMap = new Map(boundaries.map(b => [b.id, b]));
|
||||||
|
const boundaryOptions = boundaries.map(b => ({ id: b.id, name: b.name }));
|
||||||
const nodesConnId = scope.nodes?.__id ?? "";
|
const nodesConnId = scope.nodes?.__id ?? "";
|
||||||
|
const boundariesConnId = scope.boundaries?.__id ?? "";
|
||||||
const processesConnId = scope.processes?.__id ?? "";
|
const processesConnId = scope.processes?.__id ?? "";
|
||||||
const threatsConnId = scope.threats?.__id ?? "";
|
const threatsConnId = scope.threats?.__id ?? "";
|
||||||
const scenariosConnId = scope.scenarios?.__id ?? "";
|
const scenariosConnId = scope.scenarios?.__id ?? "";
|
||||||
@@ -182,13 +195,14 @@ export function ScopeCard(props: {
|
|||||||
title={`${__("Nodes")} (${nodes.length})`}
|
title={`${__("Nodes")} (${nodes.length})`}
|
||||||
hint={__("Entities, boundaries, assets, and data involved in this scope.")}
|
hint={__("Entities, boundaries, assets, and data involved in this scope.")}
|
||||||
>
|
>
|
||||||
<CreateNodeDialog scopeId={scope.id} connectionId={nodesConnId} />
|
<CreateNodeDialog scopeId={scope.id} connectionId={nodesConnId} boundaries={boundaryOptions} />
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th>{__("Name")}</Th>
|
||||||
<Th>{__("Type")}</Th>
|
<Th>{__("Type")}</Th>
|
||||||
|
<Th>{__("Boundary")}</Th>
|
||||||
<Th className="w-12" />
|
<Th className="w-12" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
@@ -197,9 +211,16 @@ export function ScopeCard(props: {
|
|||||||
<Tr key={node.id}>
|
<Tr key={node.id}>
|
||||||
<Td className="font-medium">{node.name}</Td>
|
<Td className="font-medium">{node.name}</Td>
|
||||||
<Td><Badge>{node.nodeType}</Badge></Td>
|
<Td><Badge>{node.nodeType}</Badge></Td>
|
||||||
|
<Td className="text-txt-secondary">{node.boundaryId ? boundaryMap.get(node.boundaryId)?.name ?? "—" : "—"}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<NodeActions
|
<NodeActions
|
||||||
node={{ id: node.id, name: node.name, nodeType: node.nodeType }}
|
node={{
|
||||||
|
id: node.id,
|
||||||
|
name: node.name,
|
||||||
|
nodeType: node.nodeType,
|
||||||
|
boundaryId: node.boundaryId ?? null,
|
||||||
|
}}
|
||||||
|
boundaries={boundaryOptions}
|
||||||
connectionId={nodesConnId}
|
connectionId={nodesConnId}
|
||||||
/>
|
/>
|
||||||
</Td>
|
</Td>
|
||||||
@@ -207,7 +228,7 @@ export function ScopeCard(props: {
|
|||||||
))}
|
))}
|
||||||
{nodes.length === 0 && (
|
{nodes.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={3} className="text-center text-txt-secondary">{__("No nodes")}</Td>
|
<Td colSpan={4} className="text-center text-txt-secondary">{__("No nodes")}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
@@ -264,6 +285,52 @@ export function ScopeCard(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SectionHeader
|
||||||
|
title={`${__("Boundaries")} (${boundaries.length})`}
|
||||||
|
hint={__("Groupings that contain nodes and can be nested inside other boundaries.")}
|
||||||
|
>
|
||||||
|
<CreateBoundaryDialog
|
||||||
|
scopeId={scope.id}
|
||||||
|
connectionId={boundariesConnId}
|
||||||
|
boundaries={boundaryOptions}
|
||||||
|
/>
|
||||||
|
</SectionHeader>
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{__("Name")}</Th>
|
||||||
|
<Th>{__("Parent")}</Th>
|
||||||
|
<Th className="w-12" />
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{boundaries.map(boundary => (
|
||||||
|
<Tr key={boundary.id}>
|
||||||
|
<Td className="font-medium">{boundary.name}</Td>
|
||||||
|
<Td className="text-txt-secondary">{boundary.parentBoundaryId ? boundaryMap.get(boundary.parentBoundaryId)?.name ?? "—" : "—"}</Td>
|
||||||
|
<Td>
|
||||||
|
<BoundaryActions
|
||||||
|
boundary={{
|
||||||
|
id: boundary.id,
|
||||||
|
name: boundary.name,
|
||||||
|
parentBoundaryId: boundary.parentBoundaryId ?? null,
|
||||||
|
}}
|
||||||
|
boundaries={boundaryOptions}
|
||||||
|
connectionId={boundariesConnId}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
{boundaries.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={3} className="text-center text-txt-secondary">{__("No boundaries")}</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title={`${__("Threats")} (${threats.length})`}
|
title={`${__("Threats")} (${threats.length})`}
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ const scopeDiagramFragment = graphql`
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
nodeType
|
nodeType
|
||||||
|
boundaryId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boundaries(first: 100)
|
||||||
|
@connection(key: "RiskAssessmentScope_boundaries", filters: []) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
parentBoundaryId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +93,10 @@ export function ScopeDiagram({ scopeKey }: ScopeDiagramProps) {
|
|||||||
const mermaidChart = scope.mermaidChart;
|
const mermaidChart = scope.mermaidChart;
|
||||||
|
|
||||||
const nodeSignature = scope.nodes?.edges
|
const nodeSignature = scope.nodes?.edges
|
||||||
.map(e => `${e.node.id}|${e.node.name}|${e.node.nodeType}`)
|
.map(e => `${e.node.id}|${e.node.name}|${e.node.nodeType}|${e.node.boundaryId ?? ""}`)
|
||||||
|
.join(";") ?? "";
|
||||||
|
const boundarySignature = scope.boundaries?.edges
|
||||||
|
.map(e => `${e.node.id}|${e.node.name}|${e.node.parentBoundaryId ?? ""}`)
|
||||||
.join(";") ?? "";
|
.join(";") ?? "";
|
||||||
const processSignature = scope.processes?.edges
|
const processSignature = scope.processes?.edges
|
||||||
.map(e => `${e.node.id}|${e.node.name}|${e.node.sourceNodeId}|${e.node.targetNodeId}`)
|
.map(e => `${e.node.id}|${e.node.name}|${e.node.sourceNodeId}|${e.node.targetNodeId}`)
|
||||||
@@ -90,7 +104,7 @@ export function ScopeDiagram({ scopeKey }: ScopeDiagramProps) {
|
|||||||
const threatSignature = scope.threats?.edges
|
const threatSignature = scope.threats?.edges
|
||||||
.map(e => `${e.node.id}|${e.node.name}|${e.node.processId}|${e.node.category}`)
|
.map(e => `${e.node.id}|${e.node.name}|${e.node.processId}|${e.node.category}`)
|
||||||
.join(";") ?? "";
|
.join(";") ?? "";
|
||||||
const signature = `${nodeSignature}::${processSignature}::${threatSignature}`;
|
const signature = `${nodeSignature}::${boundarySignature}::${processSignature}::${threatSignature}`;
|
||||||
const previousSignature = useRef(signature);
|
const previousSignature = useRef(signature);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousSignature.current === signature) {
|
if (previousSignature.current === signature) {
|
||||||
@@ -188,7 +202,7 @@ function Legend() {
|
|||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const items: LegendItem[] = [
|
const items: LegendItem[] = [
|
||||||
{ label: __("Entity"), shape: "stadium", fill: "#dbeafe", stroke: "#1d4ed8", text: "#1e3a8a" },
|
{ label: __("Entity"), shape: "stadium", fill: "#dbeafe", stroke: "#1d4ed8", text: "#1e3a8a" },
|
||||||
{ label: __("Boundary"), shape: "hexagon", fill: "#fef3c7", stroke: "#b45309", text: "#78350f" },
|
{ label: __("Boundary"), shape: "rectangle", fill: "#ffffff", stroke: "#b45309", text: "#78350f" },
|
||||||
{ label: __("Asset"), shape: "rectangle", fill: "#e5e7eb", stroke: "#374151", text: "#111827" },
|
{ label: __("Asset"), shape: "rectangle", fill: "#e5e7eb", stroke: "#374151", text: "#111827" },
|
||||||
{ label: __("Data"), shape: "cylinder", fill: "#dcfce7", stroke: "#15803d", text: "#14532d" },
|
{ label: __("Data"), shape: "cylinder", fill: "#dcfce7", stroke: "#15803d", text: "#14532d" },
|
||||||
{ label: __("Threat"), shape: "hexagon", fill: "#fee2e2", stroke: "#b91c1c", text: "#7f1d1d" },
|
{ label: __("Threat"), shape: "hexagon", fill: "#fee2e2", stroke: "#b91c1c", text: "#7f1d1d" },
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func TestRiskAssessmentScope_CRUD(t *testing.T) {
|
|||||||
func TestRiskAssessmentNode_Create(t *testing.T) {
|
func TestRiskAssessmentNode_Create(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, nodeType := range []string{"ENTITY", "BOUNDARY", "ASSET", "DATA"} {
|
for _, nodeType := range []string{"ENTITY", "ASSET", "DATA"} {
|
||||||
t.Run("nodeType="+nodeType, func(t *testing.T) {
|
t.Run("nodeType="+nodeType, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
@@ -479,13 +479,13 @@ func TestRiskAssessmentNode_Update(t *testing.T) {
|
|||||||
"input": map[string]any{
|
"input": map[string]any{
|
||||||
"id": nodeID,
|
"id": nodeID,
|
||||||
"name": "Updated node",
|
"name": "Updated node",
|
||||||
"nodeType": "BOUNDARY",
|
"nodeType": "ASSET",
|
||||||
},
|
},
|
||||||
}, &result)
|
}, &result)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "Updated node", result.UpdateRiskAssessmentNode.RiskAssessmentNode.Name)
|
assert.Equal(t, "Updated node", result.UpdateRiskAssessmentNode.RiskAssessmentNode.Name)
|
||||||
assert.Equal(t, "BOUNDARY", result.UpdateRiskAssessmentNode.RiskAssessmentNode.NodeType)
|
assert.Equal(t, "ASSET", result.UpdateRiskAssessmentNode.RiskAssessmentNode.NodeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRiskAssessmentProcess_Update(t *testing.T) {
|
func TestRiskAssessmentProcess_Update(t *testing.T) {
|
||||||
@@ -779,3 +779,316 @@ func TestRiskAssessment_TenantIsolation(t *testing.T) {
|
|||||||
`, map[string]any{"id": raID}, &result)
|
`, map[string]any{"id": raID}, &result)
|
||||||
testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "RiskAssessment")
|
testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "RiskAssessment")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentBoundary_Create(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
parentID := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "External Zone"})
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
CreateRiskAssessmentBoundary struct {
|
||||||
|
RiskAssessmentBoundaryEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ParentBoundaryID *string `json:"parentBoundaryId"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"riskAssessmentBoundaryEdge"`
|
||||||
|
} `json:"createRiskAssessmentBoundary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(`
|
||||||
|
mutation($input: CreateRiskAssessmentBoundaryInput!) {
|
||||||
|
createRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundaryEdge { node { id name parentBoundaryId } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"riskAssessmentScopeId": scopeID,
|
||||||
|
"parentBoundaryId": parentID,
|
||||||
|
"name": "Internal Network",
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node := result.CreateRiskAssessmentBoundary.RiskAssessmentBoundaryEdge.Node
|
||||||
|
assert.NotEmpty(t, node.ID)
|
||||||
|
assert.Equal(t, "Internal Network", node.Name)
|
||||||
|
require.NotNil(t, node.ParentBoundaryID)
|
||||||
|
assert.Equal(t, parentID, *node.ParentBoundaryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentBoundary_ListViaScope(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Zone A"})
|
||||||
|
factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Zone B"})
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Node struct {
|
||||||
|
Boundaries struct {
|
||||||
|
TotalCount int `json:"totalCount"`
|
||||||
|
Edges []struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"edges"`
|
||||||
|
} `json:"boundaries"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(`
|
||||||
|
query($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on RiskAssessmentScope {
|
||||||
|
boundaries(first: 10) {
|
||||||
|
totalCount
|
||||||
|
edges { node { id name } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{"id": scopeID}, &result)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, result.Node.Boundaries.TotalCount)
|
||||||
|
assert.Len(t, result.Node.Boundaries.Edges, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentBoundary_Update(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
parentID := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Parent"})
|
||||||
|
boundaryID := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Original"})
|
||||||
|
|
||||||
|
const mutation = `
|
||||||
|
mutation($input: UpdateRiskAssessmentBoundaryInput!) {
|
||||||
|
updateRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundary { id name parentBoundaryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
UpdateRiskAssessmentBoundary struct {
|
||||||
|
RiskAssessmentBoundary struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ParentBoundaryID *string `json:"parentBoundaryId"`
|
||||||
|
} `json:"riskAssessmentBoundary"`
|
||||||
|
} `json:"updateRiskAssessmentBoundary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename and assign a parent.
|
||||||
|
err := owner.Execute(mutation, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"id": boundaryID,
|
||||||
|
"name": "Renamed",
|
||||||
|
"parentBoundaryId": parentID,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Renamed", result.UpdateRiskAssessmentBoundary.RiskAssessmentBoundary.Name)
|
||||||
|
require.NotNil(t, result.UpdateRiskAssessmentBoundary.RiskAssessmentBoundary.ParentBoundaryID)
|
||||||
|
assert.Equal(t, parentID, *result.UpdateRiskAssessmentBoundary.RiskAssessmentBoundary.ParentBoundaryID)
|
||||||
|
|
||||||
|
// Clear the parent (move back to the top level).
|
||||||
|
err = owner.Execute(mutation, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"id": boundaryID,
|
||||||
|
"parentBoundaryId": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, result.UpdateRiskAssessmentBoundary.RiskAssessmentBoundary.ParentBoundaryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentBoundary_PreventCycle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
a := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "A"})
|
||||||
|
b := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "B", "parentBoundaryId": a})
|
||||||
|
|
||||||
|
// B is nested under A, so nesting A under B would create a cycle.
|
||||||
|
_, err := owner.Do(`
|
||||||
|
mutation($input: UpdateRiskAssessmentBoundaryInput!) {
|
||||||
|
updateRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundary { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"id": a,
|
||||||
|
"parentBoundaryId": b,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Error(t, err, "nesting a boundary under its own descendant should be rejected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentBoundary_Delete(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
parentID := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Parent"})
|
||||||
|
childID := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Child", "parentBoundaryId": parentID})
|
||||||
|
nodeID := factory.CreateRiskAssessmentNode(owner, scopeID, factory.Attrs{"name": "Member", "boundaryId": parentID})
|
||||||
|
|
||||||
|
_, err := owner.Do(`
|
||||||
|
mutation($input: DeleteRiskAssessmentBoundaryInput!) {
|
||||||
|
deleteRiskAssessmentBoundary(input: $input) { deletedRiskAssessmentBoundaryId }
|
||||||
|
}
|
||||||
|
`, map[string]any{"input": map[string]any{"riskAssessmentBoundaryId": parentID}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Deleting a parent moves its nested boundary and member node to the top
|
||||||
|
// level instead of cascading the delete (ON DELETE SET NULL).
|
||||||
|
var result struct {
|
||||||
|
Child *struct {
|
||||||
|
ParentBoundaryID *string `json:"parentBoundaryId"`
|
||||||
|
} `json:"child"`
|
||||||
|
Member *struct {
|
||||||
|
BoundaryID *string `json:"boundaryId"`
|
||||||
|
} `json:"member"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(`
|
||||||
|
query($child: ID!, $member: ID!) {
|
||||||
|
child: node(id: $child) { ... on RiskAssessmentBoundary { parentBoundaryId } }
|
||||||
|
member: node(id: $member) { ... on RiskAssessmentNode { boundaryId } }
|
||||||
|
}
|
||||||
|
`, map[string]any{"child": childID, "member": nodeID}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result.Child)
|
||||||
|
assert.Nil(t, result.Child.ParentBoundaryID)
|
||||||
|
require.NotNil(t, result.Member)
|
||||||
|
assert.Nil(t, result.Member.BoundaryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentNode_WithBoundary(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
boundaryID := factory.CreateRiskAssessmentBoundary(owner, scopeID)
|
||||||
|
|
||||||
|
var createResult struct {
|
||||||
|
CreateRiskAssessmentNode struct {
|
||||||
|
RiskAssessmentNodeEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
BoundaryID *string `json:"boundaryId"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"riskAssessmentNodeEdge"`
|
||||||
|
} `json:"createRiskAssessmentNode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(`
|
||||||
|
mutation($input: CreateRiskAssessmentNodeInput!) {
|
||||||
|
createRiskAssessmentNode(input: $input) {
|
||||||
|
riskAssessmentNodeEdge { node { id boundaryId } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"riskAssessmentScopeId": scopeID,
|
||||||
|
"nodeType": "ASSET",
|
||||||
|
"name": "Member",
|
||||||
|
"boundaryId": boundaryID,
|
||||||
|
},
|
||||||
|
}, &createResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
created := createResult.CreateRiskAssessmentNode.RiskAssessmentNodeEdge.Node
|
||||||
|
require.NotNil(t, created.BoundaryID)
|
||||||
|
assert.Equal(t, boundaryID, *created.BoundaryID)
|
||||||
|
|
||||||
|
// Clearing boundaryId moves the node back to the top level.
|
||||||
|
var updateResult struct {
|
||||||
|
UpdateRiskAssessmentNode struct {
|
||||||
|
RiskAssessmentNode struct {
|
||||||
|
BoundaryID *string `json:"boundaryId"`
|
||||||
|
} `json:"riskAssessmentNode"`
|
||||||
|
} `json:"updateRiskAssessmentNode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(`
|
||||||
|
mutation($input: UpdateRiskAssessmentNodeInput!) {
|
||||||
|
updateRiskAssessmentNode(input: $input) {
|
||||||
|
riskAssessmentNode { boundaryId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"id": created.ID,
|
||||||
|
"boundaryId": nil,
|
||||||
|
},
|
||||||
|
}, &updateResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, updateResult.UpdateRiskAssessmentNode.RiskAssessmentNode.BoundaryID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRiskAssessmentBoundary_RBAC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("viewer cannot create", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
|
||||||
|
_, err := viewer.Do(`
|
||||||
|
mutation($input: CreateRiskAssessmentBoundaryInput!) {
|
||||||
|
createRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundaryEdge { node { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"riskAssessmentScopeId": scopeID,
|
||||||
|
"name": "Nope",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testutil.RequireForbiddenError(t, err, "viewer cannot create risk assessment boundary")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("viewer can read", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||||
|
raID := factory.CreateRiskAssessment(owner)
|
||||||
|
scopeID := factory.CreateRiskAssessmentScope(owner, raID)
|
||||||
|
boundaryID := factory.CreateRiskAssessmentBoundary(owner, scopeID, factory.Attrs{"name": "Visible"})
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"node"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := viewer.Execute(`
|
||||||
|
query($id: ID!) { node(id: $id) { ... on RiskAssessmentBoundary { id name } } }
|
||||||
|
`, map[string]any{"id": boundaryID}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "Visible", result.Node.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1598,6 +1598,10 @@ func CreateRiskAssessmentNode(c *testutil.Client, scopeID string, attrs ...Attrs
|
|||||||
"name": a.getString("name", SafeName("Node")),
|
"name": a.getString("name", SafeName("Node")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if boundaryID := a.getString("boundaryId", ""); boundaryID != "" {
|
||||||
|
input["boundaryId"] = boundaryID
|
||||||
|
}
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
CreateRiskAssessmentNode struct {
|
CreateRiskAssessmentNode struct {
|
||||||
RiskAssessmentNodeEdge struct {
|
RiskAssessmentNodeEdge struct {
|
||||||
@@ -1614,6 +1618,47 @@ func CreateRiskAssessmentNode(c *testutil.Client, scopeID string, attrs ...Attrs
|
|||||||
return result.CreateRiskAssessmentNode.RiskAssessmentNodeEdge.Node.ID
|
return result.CreateRiskAssessmentNode.RiskAssessmentNodeEdge.Node.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateRiskAssessmentBoundary(c *testutil.Client, scopeID string, attrs ...Attrs) string {
|
||||||
|
c.T.Helper()
|
||||||
|
|
||||||
|
var a Attrs
|
||||||
|
if len(attrs) > 0 {
|
||||||
|
a = attrs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
mutation($input: CreateRiskAssessmentBoundaryInput!) {
|
||||||
|
createRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundaryEdge { node { id } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"riskAssessmentScopeId": scopeID,
|
||||||
|
"name": a.getString("name", SafeName("Boundary")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if parentID := a.getString("parentBoundaryId", ""); parentID != "" {
|
||||||
|
input["parentBoundaryId"] = parentID
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
CreateRiskAssessmentBoundary struct {
|
||||||
|
RiskAssessmentBoundaryEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"riskAssessmentBoundaryEdge"`
|
||||||
|
} `json:"createRiskAssessmentBoundary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Execute(query, map[string]any{"input": input}, &result)
|
||||||
|
require.NoError(c.T, err, "createRiskAssessmentBoundary mutation failed")
|
||||||
|
|
||||||
|
return result.CreateRiskAssessmentBoundary.RiskAssessmentBoundaryEdge.Node.ID
|
||||||
|
}
|
||||||
|
|
||||||
func CreateRiskAssessmentProcess(c *testutil.Client, scopeID, sourceNodeID, targetNodeID string, attrs ...Attrs) string {
|
func CreateRiskAssessmentProcess(c *testutil.Client, scopeID, sourceNodeID, targetNodeID string, attrs ...Attrs) string {
|
||||||
c.T.Helper()
|
c.T.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -45,10 +45,6 @@ export const description: INodeProperties[] = [
|
|||||||
name: 'Entity',
|
name: 'Entity',
|
||||||
value: 'ENTITY',
|
value: 'ENTITY',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Boundary',
|
|
||||||
value: 'BOUNDARY',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Asset',
|
name: 'Asset',
|
||||||
value: 'ASSET',
|
value: 'ASSET',
|
||||||
@@ -76,6 +72,19 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'The name of the node',
|
description: 'The name of the node',
|
||||||
required: true,
|
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(
|
export async function execute(
|
||||||
@@ -85,6 +94,7 @@ export async function execute(
|
|||||||
const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string;
|
const riskAssessmentScopeId = this.getNodeParameter('riskAssessmentScopeId', itemIndex) as string;
|
||||||
const nodeType = this.getNodeParameter('nodeType', itemIndex) as string;
|
const nodeType = this.getNodeParameter('nodeType', itemIndex) as string;
|
||||||
const name = this.getNodeParameter('name', itemIndex) as string;
|
const name = this.getNodeParameter('name', itemIndex) as string;
|
||||||
|
const boundaryId = this.getNodeParameter('boundaryId', itemIndex, '') as string;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
mutation CreateRiskAssessmentNode($input: CreateRiskAssessmentNodeInput!) {
|
mutation CreateRiskAssessmentNode($input: CreateRiskAssessmentNodeInput!) {
|
||||||
@@ -93,6 +103,7 @@ export async function execute(
|
|||||||
node {
|
node {
|
||||||
id
|
id
|
||||||
riskAssessmentScopeId
|
riskAssessmentScopeId
|
||||||
|
boundaryId
|
||||||
nodeType
|
nodeType
|
||||||
name
|
name
|
||||||
createdAt
|
createdAt
|
||||||
@@ -103,9 +114,10 @@ export async function execute(
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const responseData = await proboApiRequest.call(this, query, {
|
const input: Record<string, unknown> = { riskAssessmentScopeId, nodeType, name };
|
||||||
input: { riskAssessmentScopeId, nodeType, name },
|
if (boundaryId) input.boundaryId = boundaryId;
|
||||||
});
|
|
||||||
|
const responseData = await proboApiRequest.call(this, query, { input });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
json: responseData,
|
json: responseData,
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -29,6 +29,11 @@ import * as getNodeOp from './getNode.operation';
|
|||||||
import * as getAllNodesOp from './getAllNodes.operation';
|
import * as getAllNodesOp from './getAllNodes.operation';
|
||||||
import * as updateNodeOp from './updateNode.operation';
|
import * as updateNodeOp from './updateNode.operation';
|
||||||
import * as deleteNodeOp from './deleteNode.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 createProcessOp from './createProcess.operation';
|
||||||
import * as getProcessOp from './getProcess.operation';
|
import * as getProcessOp from './getProcess.operation';
|
||||||
import * as getAllProcessesOp from './getAllProcesses.operation';
|
import * as getAllProcessesOp from './getAllProcesses.operation';
|
||||||
@@ -67,6 +72,12 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Create a risk assessment',
|
description: 'Create a risk assessment',
|
||||||
action: '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',
|
name: 'Create Node',
|
||||||
value: 'createNode',
|
value: 'createNode',
|
||||||
@@ -103,6 +114,12 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Delete a risk assessment',
|
description: 'Delete a risk assessment',
|
||||||
action: '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',
|
name: 'Delete Node',
|
||||||
value: 'deleteNode',
|
value: 'deleteNode',
|
||||||
@@ -139,12 +156,23 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Get a risk assessment',
|
description: 'Get a risk assessment',
|
||||||
action: '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',
|
name: 'Get Many',
|
||||||
value: 'getAll',
|
value: 'getAll',
|
||||||
description: 'Get many risk assessments',
|
description: 'Get many risk assessments',
|
||||||
action: 'Get many risk assessments',
|
action: 'Get many risk assessments',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Get Many Boundaries',
|
||||||
|
value: 'getAllBoundaries',
|
||||||
|
action: 'Get many boundaries',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Get Many Nodes',
|
name: 'Get Many Nodes',
|
||||||
value: 'getAllNodes',
|
value: 'getAllNodes',
|
||||||
@@ -236,6 +264,12 @@ export const description: INodeProperties[] = [
|
|||||||
description: 'Update a risk assessment',
|
description: 'Update a risk assessment',
|
||||||
action: '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',
|
name: 'Update Node',
|
||||||
value: 'updateNode',
|
value: 'updateNode',
|
||||||
@@ -285,6 +319,11 @@ export const description: INodeProperties[] = [
|
|||||||
...getAllNodesOp.description,
|
...getAllNodesOp.description,
|
||||||
...updateNodeOp.description,
|
...updateNodeOp.description,
|
||||||
...deleteNodeOp.description,
|
...deleteNodeOp.description,
|
||||||
|
...createBoundaryOp.description,
|
||||||
|
...getBoundaryOp.description,
|
||||||
|
...getAllBoundariesOp.description,
|
||||||
|
...updateBoundaryOp.description,
|
||||||
|
...deleteBoundaryOp.description,
|
||||||
...createProcessOp.description,
|
...createProcessOp.description,
|
||||||
...getProcessOp.description,
|
...getProcessOp.description,
|
||||||
...getAllProcessesOp.description,
|
...getAllProcessesOp.description,
|
||||||
@@ -323,6 +362,11 @@ export {
|
|||||||
getAllNodesOp as getAllNodes,
|
getAllNodesOp as getAllNodes,
|
||||||
updateNodeOp as updateNode,
|
updateNodeOp as updateNode,
|
||||||
deleteNodeOp as deleteNode,
|
deleteNodeOp as deleteNode,
|
||||||
|
createBoundaryOp as createBoundary,
|
||||||
|
getBoundaryOp as getBoundary,
|
||||||
|
getAllBoundariesOp as getAllBoundaries,
|
||||||
|
updateBoundaryOp as updateBoundary,
|
||||||
|
deleteBoundaryOp as deleteBoundary,
|
||||||
createProcessOp as createProcess,
|
createProcessOp as createProcess,
|
||||||
getProcessOp as getProcess,
|
getProcessOp as getProcess,
|
||||||
getAllProcessesOp as getAllProcesses,
|
getAllProcessesOp as getAllProcesses,
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -59,10 +59,6 @@ export const description: INodeProperties[] = [
|
|||||||
name: 'Entity',
|
name: 'Entity',
|
||||||
value: 'ENTITY',
|
value: 'ENTITY',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Boundary',
|
|
||||||
value: 'BOUNDARY',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Asset',
|
name: 'Asset',
|
||||||
value: 'ASSET',
|
value: 'ASSET',
|
||||||
@@ -75,6 +71,13 @@ export const description: INodeProperties[] = [
|
|||||||
default: 'ENTITY',
|
default: 'ENTITY',
|
||||||
description: 'The type of the node',
|
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 {
|
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as {
|
||||||
name?: string;
|
name?: string;
|
||||||
nodeType?: string;
|
nodeType?: string;
|
||||||
|
boundaryId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
@@ -95,6 +99,7 @@ export async function execute(
|
|||||||
riskAssessmentNode {
|
riskAssessmentNode {
|
||||||
id
|
id
|
||||||
riskAssessmentScopeId
|
riskAssessmentScopeId
|
||||||
|
boundaryId
|
||||||
nodeType
|
nodeType
|
||||||
name
|
name
|
||||||
createdAt
|
createdAt
|
||||||
@@ -107,6 +112,9 @@ export async function execute(
|
|||||||
const input: Record<string, unknown> = { id: nodeId };
|
const input: Record<string, unknown> = { id: nodeId };
|
||||||
if (additionalFields.name) input.name = additionalFields.name;
|
if (additionalFields.name) input.name = additionalFields.name;
|
||||||
if (additionalFields.nodeType) input.nodeType = additionalFields.nodeType;
|
if (additionalFields.nodeType) input.nodeType = additionalFields.nodeType;
|
||||||
|
if (additionalFields.boundaryId !== undefined) {
|
||||||
|
input.boundaryId = additionalFields.boundaryId || null;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(input).length === 1) {
|
if (Object.keys(input).length === 1) {
|
||||||
throw new Error('At least one field must be provided to update');
|
throw new Error('At least one field must be provided to update');
|
||||||
|
|||||||
@@ -74,3 +74,13 @@ func ValidateEnum(flag string, value string, allowed []string) error {
|
|||||||
strings.Join(allowed, ", "),
|
strings.Join(allowed, ", "),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateLimit checks that a --limit value is positive. A non-positive limit
|
||||||
|
// would otherwise cause pagination to return no results without an error.
|
||||||
|
func ValidateLimit(value int) error {
|
||||||
|
if value <= 0 {
|
||||||
|
return fmt.Errorf("invalid --limit value %d: must be greater than 0", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
40
pkg/cmd/risk-assessment/boundary/boundary.go
Normal file
40
pkg/cmd/risk-assessment/boundary/boundary.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package boundary
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/boundary/create"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/boundary/delete"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/boundary/list"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/boundary/update"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/boundary/view"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCmdBoundary(f *cmdutil.Factory) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "boundary <command>",
|
||||||
|
Short: "Manage risk assessment boundaries",
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
153
pkg/cmd/risk-assessment/boundary/create/create.go
Normal file
153
pkg/cmd/risk-assessment/boundary/create/create.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
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: CreateRiskAssessmentBoundaryInput!) {
|
||||||
|
createRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundaryEdge {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
riskAssessmentScopeId
|
||||||
|
parentBoundaryId
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type createResponse struct {
|
||||||
|
CreateRiskAssessmentBoundary struct {
|
||||||
|
RiskAssessmentBoundaryEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
RiskAssessmentScopeId string `json:"riskAssessmentScopeId"`
|
||||||
|
ParentBoundaryId *string `json:"parentBoundaryId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"riskAssessmentBoundaryEdge"`
|
||||||
|
} `json:"createRiskAssessmentBoundary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||||
|
var (
|
||||||
|
flagScopeId string
|
||||||
|
flagParentId string
|
||||||
|
flagName string
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a new risk assessment boundary",
|
||||||
|
Example: ` # Create a boundary interactively
|
||||||
|
prb risk-assessment boundary create --scope-id <id>
|
||||||
|
|
||||||
|
# Create a boundary non-interactively
|
||||||
|
prb risk-assessment boundary create --scope-id <id> --name "Production environment"
|
||||||
|
|
||||||
|
# Create a boundary nested inside another boundary
|
||||||
|
prb risk-assessment boundary create --scope-id <id> --name "Database tier" --parent-id <boundary-id>`,
|
||||||
|
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("Boundary 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 flagParentId != "" {
|
||||||
|
input["parentBoundaryId"] = flagParentId
|
||||||
|
}
|
||||||
|
|
||||||
|
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.CreateRiskAssessmentBoundary.RiskAssessmentBoundaryEdge.Node
|
||||||
|
_, _ = fmt.Fprintf(
|
||||||
|
f.IOStreams.Out,
|
||||||
|
"Created risk assessment boundary %s (%s)\n",
|
||||||
|
r.ID,
|
||||||
|
r.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&flagScopeId, "scope-id", "", "Risk assessment scope ID (required)")
|
||||||
|
cmd.Flags().StringVar(&flagParentId, "parent-id", "", "Parent boundary ID (optional, for nested boundaries)")
|
||||||
|
cmd.Flags().StringVar(&flagName, "name", "", "Boundary name (required)")
|
||||||
|
|
||||||
|
_ = cmd.MarkFlagRequired("scope-id")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
105
pkg/cmd/risk-assessment/boundary/delete/delete.go
Normal file
105
pkg/cmd/risk-assessment/boundary/delete/delete.go
Normal 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.
|
||||||
|
|
||||||
|
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: DeleteRiskAssessmentBoundaryInput!) {
|
||||||
|
deleteRiskAssessmentBoundary(input: $input) {
|
||||||
|
deletedRiskAssessmentBoundaryId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
|
||||||
|
var flagYes bool
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "delete <id>",
|
||||||
|
Short: "Delete a risk assessment boundary",
|
||||||
|
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 boundary: confirmation required, use --yes to confirm")
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmed bool
|
||||||
|
|
||||||
|
err := huh.NewConfirm().
|
||||||
|
Title(fmt.Sprintf("Delete risk assessment boundary %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{
|
||||||
|
"riskAssessmentBoundaryId": args[0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(
|
||||||
|
f.IOStreams.Out,
|
||||||
|
"Deleted risk assessment boundary %s\n",
|
||||||
|
args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
208
pkg/cmd/risk-assessment/boundary/list/list.go
Normal file
208
pkg/cmd/risk-assessment/boundary/list/list.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
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: RiskAssessmentBoundaryOrder) {
|
||||||
|
node(id: $id) {
|
||||||
|
__typename
|
||||||
|
... on RiskAssessmentScope {
|
||||||
|
boundaries(first: $first, after: $after, orderBy: $orderBy) {
|
||||||
|
totalCount
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
riskAssessmentScopeId
|
||||||
|
parentBoundaryId
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type riskAssessmentBoundary struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
RiskAssessmentScopeId string `json:"riskAssessmentScopeId"`
|
||||||
|
ParentBoundaryId *string `json:"parentBoundaryId"`
|
||||||
|
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 boundaries in a risk assessment scope",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Example: ` # List boundaries in a scope
|
||||||
|
prb risk-assessment boundary list --scope <id>
|
||||||
|
|
||||||
|
# List boundaries as JSON
|
||||||
|
prb risk-assessment boundary ls --scope <id> --json`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmdutil.ValidateLimit(flagLimit); 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boundaries, totalCount, err := api.Paginate(
|
||||||
|
client,
|
||||||
|
listQuery,
|
||||||
|
variables,
|
||||||
|
flagLimit,
|
||||||
|
func(data json.RawMessage) (*api.Connection[riskAssessmentBoundary], error) {
|
||||||
|
var resp struct {
|
||||||
|
Node *struct {
|
||||||
|
Typename string `json:"__typename"`
|
||||||
|
Boundaries api.Connection[riskAssessmentBoundary] `json:"boundaries"`
|
||||||
|
} `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.Boundaries, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if *flagOutput == cmdutil.OutputJSON {
|
||||||
|
return cmdutil.PrintJSON(f.IOStreams.Out, boundaries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(boundaries) == 0 {
|
||||||
|
_, _ = fmt.Fprintln(f.IOStreams.Out, "No boundaries found.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([][]string, 0, len(boundaries))
|
||||||
|
for _, b := range boundaries {
|
||||||
|
parent := ""
|
||||||
|
if b.ParentBoundaryId != nil {
|
||||||
|
parent = *b.ParentBoundaryId
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{
|
||||||
|
b.ID,
|
||||||
|
b.Name,
|
||||||
|
parent,
|
||||||
|
cmdutil.FormatTime(b.CreatedAt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t := cmdutil.NewTable("ID", "NAME", "PARENT", "CREATED AT").Rows(rows...)
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
|
||||||
|
|
||||||
|
if totalCount > len(boundaries) {
|
||||||
|
_, _ = fmt.Fprintf(
|
||||||
|
f.IOStreams.ErrOut,
|
||||||
|
"\nShowing %d of %d boundaries\n",
|
||||||
|
len(boundaries),
|
||||||
|
totalCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&flagScope, "scope", "", "Risk assessment scope ID (required)")
|
||||||
|
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of boundaries 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
|
||||||
|
}
|
||||||
136
pkg/cmd/risk-assessment/boundary/update/update.go
Normal file
136
pkg/cmd/risk-assessment/boundary/update/update.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
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: UpdateRiskAssessmentBoundaryInput!) {
|
||||||
|
updateRiskAssessmentBoundary(input: $input) {
|
||||||
|
riskAssessmentBoundary {
|
||||||
|
id
|
||||||
|
parentBoundaryId
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type updateResponse struct {
|
||||||
|
UpdateRiskAssessmentBoundary struct {
|
||||||
|
RiskAssessmentBoundary struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ParentBoundaryId *string `json:"parentBoundaryId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
UpdatedAt string `json:"updatedAt"`
|
||||||
|
} `json:"riskAssessmentBoundary"`
|
||||||
|
} `json:"updateRiskAssessmentBoundary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||||
|
var (
|
||||||
|
flagName string
|
||||||
|
flagParentId string
|
||||||
|
flagClearParent bool
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "update <id>",
|
||||||
|
Short: "Update a risk assessment boundary",
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
if flagClearParent && cmd.Flags().Changed("parent-id") {
|
||||||
|
return fmt.Errorf("cannot use --parent-id and --clear-parent together")
|
||||||
|
}
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
"id": args[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("name") {
|
||||||
|
input["name"] = flagName
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("parent-id") {
|
||||||
|
input["parentBoundaryId"] = flagParentId
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagClearParent {
|
||||||
|
input["parentBoundaryId"] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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.UpdateRiskAssessmentBoundary.RiskAssessmentBoundary
|
||||||
|
_, _ = fmt.Fprintf(
|
||||||
|
f.IOStreams.Out,
|
||||||
|
"Updated risk assessment boundary %s (%s)\n",
|
||||||
|
r.ID,
|
||||||
|
r.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&flagName, "name", "", "Boundary name")
|
||||||
|
cmd.Flags().StringVar(&flagParentId, "parent-id", "", "Parent boundary ID")
|
||||||
|
cmd.Flags().BoolVar(&flagClearParent, "clear-parent", false, "Remove the parent boundary (make it top-level)")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
139
pkg/cmd/risk-assessment/boundary/view/view.go
Normal file
139
pkg/cmd/risk-assessment/boundary/view/view.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
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 RiskAssessmentBoundary {
|
||||||
|
id
|
||||||
|
riskAssessmentScopeId
|
||||||
|
parentBoundaryId
|
||||||
|
name
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type viewResponse struct {
|
||||||
|
Node *struct {
|
||||||
|
Typename string `json:"__typename"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
RiskAssessmentScopeId string `json:"riskAssessmentScopeId"`
|
||||||
|
ParentBoundaryId *string `json:"parentBoundaryId"`
|
||||||
|
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 <id>",
|
||||||
|
Short: "View a risk assessment boundary",
|
||||||
|
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 boundary %s not found", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Node.Typename != "RiskAssessmentBoundary" {
|
||||||
|
return fmt.Errorf("expected RiskAssessmentBoundary 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)
|
||||||
|
|
||||||
|
parent := "(none)"
|
||||||
|
if r.ParentBoundaryId != nil {
|
||||||
|
parent = *r.ParentBoundaryId
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Parent Boundary:"), parent)
|
||||||
|
|
||||||
|
_, _ = 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
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ type createResponse struct {
|
|||||||
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||||
var (
|
var (
|
||||||
flagScopeId string
|
flagScopeId string
|
||||||
|
flagBoundaryId string
|
||||||
flagNodeType string
|
flagNodeType string
|
||||||
flagName string
|
flagName string
|
||||||
)
|
)
|
||||||
@@ -106,7 +107,6 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
Title("Node type").
|
Title("Node type").
|
||||||
Options(
|
Options(
|
||||||
huh.NewOption("Entity", "ENTITY"),
|
huh.NewOption("Entity", "ENTITY"),
|
||||||
huh.NewOption("Boundary", "BOUNDARY"),
|
|
||||||
huh.NewOption("Asset", "ASSET"),
|
huh.NewOption("Asset", "ASSET"),
|
||||||
huh.NewOption("Data", "DATA"),
|
huh.NewOption("Data", "DATA"),
|
||||||
).
|
).
|
||||||
@@ -126,7 +126,7 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
return fmt.Errorf("node type is required; pass --node-type or run interactively")
|
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 {
|
if err := cmdutil.ValidateEnum("node-type", flagNodeType, []string{"ENTITY", "ASSET", "DATA"}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +136,10 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
"name": flagName,
|
"name": flagName,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flagBoundaryId != "" {
|
||||||
|
input["boundaryId"] = flagBoundaryId
|
||||||
|
}
|
||||||
|
|
||||||
data, err := client.Do(
|
data, err := client.Do(
|
||||||
createMutation,
|
createMutation,
|
||||||
map[string]any{"input": input},
|
map[string]any{"input": input},
|
||||||
@@ -162,7 +166,8 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(&flagScopeId, "scope-id", "", "Risk assessment scope ID (required)")
|
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(&flagBoundaryId, "boundary-id", "", "Boundary ID that contains this node (optional)")
|
||||||
|
cmd.Flags().StringVar(&flagNodeType, "node-type", "", "Node type: ENTITY, ASSET, DATA (required)")
|
||||||
cmd.Flags().StringVar(&flagName, "name", "", "Node name (required)")
|
cmd.Flags().StringVar(&flagName, "name", "", "Node name (required)")
|
||||||
|
|
||||||
_ = cmd.MarkFlagRequired("scope-id")
|
_ = cmd.MarkFlagRequired("scope-id")
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
var (
|
var (
|
||||||
flagName string
|
flagName string
|
||||||
flagNodeType string
|
flagNodeType string
|
||||||
|
flagBoundaryId string
|
||||||
|
flagClearBoundary bool
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
@@ -78,6 +80,10 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if flagClearBoundary && cmd.Flags().Changed("boundary-id") {
|
||||||
|
return fmt.Errorf("cannot use --boundary-id and --clear-boundary together")
|
||||||
|
}
|
||||||
|
|
||||||
input := map[string]any{
|
input := map[string]any{
|
||||||
"id": args[0],
|
"id": args[0],
|
||||||
}
|
}
|
||||||
@@ -87,13 +93,21 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Flags().Changed("node-type") {
|
if cmd.Flags().Changed("node-type") {
|
||||||
if err := cmdutil.ValidateEnum("node-type", flagNodeType, []string{"ENTITY", "BOUNDARY", "ASSET", "DATA"}); err != nil {
|
if err := cmdutil.ValidateEnum("node-type", flagNodeType, []string{"ENTITY", "ASSET", "DATA"}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
input["nodeType"] = flagNodeType
|
input["nodeType"] = flagNodeType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flags().Changed("boundary-id") {
|
||||||
|
input["boundaryId"] = flagBoundaryId
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagClearBoundary {
|
||||||
|
input["boundaryId"] = nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(input) == 1 {
|
if len(input) == 1 {
|
||||||
return fmt.Errorf("at least one field must be specified for update")
|
return fmt.Errorf("at least one field must be specified for update")
|
||||||
}
|
}
|
||||||
@@ -124,7 +138,9 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().StringVar(&flagName, "name", "", "Node name")
|
cmd.Flags().StringVar(&flagName, "name", "", "Node name")
|
||||||
cmd.Flags().StringVar(&flagNodeType, "node-type", "", "Node type: ENTITY, BOUNDARY, ASSET, DATA")
|
cmd.Flags().StringVar(&flagNodeType, "node-type", "", "Node type: ENTITY, ASSET, DATA")
|
||||||
|
cmd.Flags().StringVar(&flagBoundaryId, "boundary-id", "", "Boundary ID that contains this node")
|
||||||
|
cmd.Flags().BoolVar(&flagClearBoundary, "clear-boundary", false, "Remove the node from its boundary (move to top level)")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package riskassessment
|
|||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
"go.probo.inc/probo/pkg/cmd/cmdutil"
|
||||||
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/boundary"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk-assessment/create"
|
"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/delete"
|
||||||
"go.probo.inc/probo/pkg/cmd/risk-assessment/list"
|
"go.probo.inc/probo/pkg/cmd/risk-assessment/list"
|
||||||
@@ -42,6 +43,7 @@ func NewCmdRiskAssessment(f *cmdutil.Factory) *cobra.Command {
|
|||||||
cmd.AddCommand(delete.NewCmdDelete(f))
|
cmd.AddCommand(delete.NewCmdDelete(f))
|
||||||
cmd.AddCommand(scope.NewCmdScope(f))
|
cmd.AddCommand(scope.NewCmdScope(f))
|
||||||
cmd.AddCommand(node.NewCmdNode(f))
|
cmd.AddCommand(node.NewCmdNode(f))
|
||||||
|
cmd.AddCommand(boundary.NewCmdBoundary(f))
|
||||||
cmd.AddCommand(process.NewCmdProcess(f))
|
cmd.AddCommand(process.NewCmdProcess(f))
|
||||||
cmd.AddCommand(threat.NewCmdThreat(f))
|
cmd.AddCommand(threat.NewCmdThreat(f))
|
||||||
cmd.AddCommand(scenario.NewCmdScenario(f))
|
cmd.AddCommand(scenario.NewCmdScenario(f))
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ const (
|
|||||||
RiskAssessmentThreatEntityType uint16 = 98
|
RiskAssessmentThreatEntityType uint16 = 98
|
||||||
RiskAssessmentScopeEntityType uint16 = 99
|
RiskAssessmentScopeEntityType uint16 = 99
|
||||||
RiskAssessmentScenarioEntityType uint16 = 100
|
RiskAssessmentScenarioEntityType uint16 = 100
|
||||||
|
RiskAssessmentBoundaryEntityType uint16 = 101
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewEntityFromID(id gid.GID) (any, bool) {
|
func NewEntityFromID(id gid.GID) (any, bool) {
|
||||||
@@ -312,6 +313,8 @@ func NewEntityFromID(id gid.GID) (any, bool) {
|
|||||||
return &RiskAssessmentScope{ID: id}, true
|
return &RiskAssessmentScope{ID: id}, true
|
||||||
case RiskAssessmentScenarioEntityType:
|
case RiskAssessmentScenarioEntityType:
|
||||||
return &RiskAssessmentScenario{ID: id}, true
|
return &RiskAssessmentScenario{ID: id}, true
|
||||||
|
case RiskAssessmentBoundaryEntityType:
|
||||||
|
return &RiskAssessmentBoundary{ID: id}, true
|
||||||
default:
|
default:
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
70
pkg/coredata/migrations/20260608T122627Z.sql
Normal file
70
pkg/coredata/migrations/20260608T122627Z.sql
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
-- 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.
|
||||||
|
|
||||||
|
CREATE TABLE risk_assessment_boundaries (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tenant_id TEXT NOT NULL,
|
||||||
|
organization_id TEXT NOT NULL,
|
||||||
|
risk_assessment_scope_id TEXT NOT NULL REFERENCES risk_assessment_scopes(id) ON DELETE CASCADE,
|
||||||
|
parent_boundary_id TEXT REFERENCES risk_assessment_boundaries(id) ON DELETE SET NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
CONSTRAINT risk_assessment_boundaries_unique_name UNIQUE (risk_assessment_scope_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE risk_assessment_nodes
|
||||||
|
ADD COLUMN boundary_id TEXT REFERENCES risk_assessment_boundaries(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Migrate legacy BOUNDARY-typed nodes into the first-class boundary model.
|
||||||
|
-- Each boundary gets a freshly generated GID (entity type 101). Node names are
|
||||||
|
-- already unique per scope, so the boundary (scope_id, name) constraint cannot
|
||||||
|
-- be violated.
|
||||||
|
INSERT INTO risk_assessment_boundaries (
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
risk_assessment_scope_id,
|
||||||
|
parent_boundary_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
generate_gid(decode_base64_unpadded(tenant_id), 101),
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
risk_assessment_scope_id,
|
||||||
|
NULL,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM risk_assessment_nodes
|
||||||
|
WHERE node_type = 'BOUNDARY';
|
||||||
|
|
||||||
|
-- Remove the legacy node representation now that the boundaries exist.
|
||||||
|
DELETE FROM risk_assessment_nodes
|
||||||
|
WHERE node_type = 'BOUNDARY';
|
||||||
|
|
||||||
|
-- Drop the now-unused BOUNDARY value from the node_type enum. PostgreSQL cannot
|
||||||
|
-- remove a value from an enum in place, so the type is rebuilt without it.
|
||||||
|
ALTER TYPE risk_assessment_node_type RENAME TO risk_assessment_node_type_old;
|
||||||
|
|
||||||
|
CREATE TYPE risk_assessment_node_type AS ENUM ('ENTITY', 'ASSET', 'DATA');
|
||||||
|
|
||||||
|
ALTER TABLE risk_assessment_nodes
|
||||||
|
ALTER COLUMN node_type TYPE risk_assessment_node_type
|
||||||
|
USING node_type::text::risk_assessment_node_type;
|
||||||
|
|
||||||
|
DROP TYPE risk_assessment_node_type_old;
|
||||||
344
pkg/coredata/risk_assessment_boundary.go
Normal file
344
pkg/coredata/risk_assessment_boundary.go
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/iam/policy"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
RiskAssessmentBoundary struct {
|
||||||
|
ID gid.GID `db:"id"`
|
||||||
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
|
RiskAssessmentScopeID gid.GID `db:"risk_assessment_scope_id"`
|
||||||
|
ParentBoundaryID *gid.GID `db:"parent_boundary_id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
CreatedAt time.Time `db:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
RiskAssessmentBoundaries []*RiskAssessmentBoundary
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *RiskAssessmentBoundary) CursorKey(orderBy RiskAssessmentBoundaryOrderField) page.CursorKey {
|
||||||
|
switch orderBy {
|
||||||
|
case RiskAssessmentBoundaryOrderFieldCreatedAt:
|
||||||
|
return page.CursorKey{ID: b.ID, Value: b.CreatedAt}
|
||||||
|
case RiskAssessmentBoundaryOrderFieldName:
|
||||||
|
return page.CursorKey{ID: b.ID, Value: b.Name}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RiskAssessmentBoundary) AuthorizationAttributes(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
resourceIDs []gid.GID,
|
||||||
|
) (policy.AttributesByID, error) {
|
||||||
|
q := `SELECT id, organization_id FROM risk_assessment_boundaries WHERE id = ANY(@resource_ids::text[])`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"resource_ids": resourceIDs,
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot query authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
attrsByID := make(policy.AttributesByID)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id, organizationID gid.GID
|
||||||
|
|
||||||
|
if err := rows.Scan(&id, &organizationID); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrsByID[id] = policy.Attributes{
|
||||||
|
"organization_id": organizationID.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot iterate authorization attributes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrsByID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *RiskAssessmentBoundaries) LoadByRiskAssessmentScopeID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
riskAssessmentScopeID gid.GID,
|
||||||
|
cursor *page.Cursor[RiskAssessmentBoundaryOrderField],
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
risk_assessment_scope_id,
|
||||||
|
parent_boundary_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
risk_assessment_boundaries
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND risk_assessment_scope_id = @risk_assessment_scope_id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), cursor.SQLFragment())
|
||||||
|
args := pgx.NamedArgs{"risk_assessment_scope_id": riskAssessmentScopeID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, cursor.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[RiskAssessmentBoundary])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*bs = results
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *RiskAssessmentBoundaries) LoadAllByRiskAssessmentScopeID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
riskAssessmentScopeID gid.GID,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
risk_assessment_scope_id,
|
||||||
|
parent_boundary_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
risk_assessment_boundaries
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND risk_assessment_scope_id = @risk_assessment_scope_id
|
||||||
|
ORDER BY
|
||||||
|
created_at ASC, id ASC
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.NamedArgs{"risk_assessment_scope_id": riskAssessmentScopeID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[RiskAssessmentBoundary])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*bs = results
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *RiskAssessmentBoundaries) CountByRiskAssessmentScopeID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Querier,
|
||||||
|
scope Scoper,
|
||||||
|
riskAssessmentScopeID gid.GID,
|
||||||
|
) (int, error) {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
COUNT(id)
|
||||||
|
FROM
|
||||||
|
risk_assessment_boundaries
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND risk_assessment_scope_id = @risk_assessment_scope_id
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.NamedArgs{"risk_assessment_scope_id": riskAssessmentScopeID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
var count int
|
||||||
|
if err := conn.QueryRow(ctx, q, args).Scan(&count); err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot count risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RiskAssessmentBoundary) LoadByID(ctx context.Context, conn pg.Querier, scope Scoper, id gid.GID) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
organization_id,
|
||||||
|
risk_assessment_scope_id,
|
||||||
|
parent_boundary_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
risk_assessment_boundaries
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
LIMIT 1;
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.StrictNamedArgs{"id": id}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[RiskAssessmentBoundary])
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot collect risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*b = result
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RiskAssessmentBoundary) Insert(ctx context.Context, conn pg.Tx, scope Scoper) error {
|
||||||
|
q := `
|
||||||
|
INSERT INTO risk_assessment_boundaries (
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
organization_id,
|
||||||
|
risk_assessment_scope_id,
|
||||||
|
parent_boundary_id,
|
||||||
|
name,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
@id,
|
||||||
|
@tenant_id,
|
||||||
|
@organization_id,
|
||||||
|
@risk_assessment_scope_id,
|
||||||
|
@parent_boundary_id,
|
||||||
|
@name,
|
||||||
|
@created_at,
|
||||||
|
@updated_at
|
||||||
|
)
|
||||||
|
`
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": b.ID,
|
||||||
|
"tenant_id": scope.GetTenantID(),
|
||||||
|
"organization_id": b.OrganizationID,
|
||||||
|
"risk_assessment_scope_id": b.RiskAssessmentScopeID,
|
||||||
|
"parent_boundary_id": b.ParentBoundaryID,
|
||||||
|
"name": b.Name,
|
||||||
|
"created_at": b.CreatedAt,
|
||||||
|
"updated_at": b.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" && pgErr.ConstraintName == "risk_assessment_boundaries_unique_name" {
|
||||||
|
return ErrResourceAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot insert risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RiskAssessmentBoundary) Update(ctx context.Context, conn pg.Tx, scope Scoper) error {
|
||||||
|
q := `
|
||||||
|
UPDATE risk_assessment_boundaries
|
||||||
|
SET
|
||||||
|
parent_boundary_id = @parent_boundary_id,
|
||||||
|
name = @name,
|
||||||
|
updated_at = @updated_at
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"id": b.ID,
|
||||||
|
"parent_boundary_id": b.ParentBoundaryID,
|
||||||
|
"name": b.Name,
|
||||||
|
"updated_at": b.UpdatedAt,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
result, err := conn.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
if pgErr, ok := errors.AsType[*pgconn.PgError](err); ok && pgErr.Code == "23505" && pgErr.ConstraintName == "risk_assessment_boundaries_unique_name" {
|
||||||
|
return ErrResourceAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("cannot update risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected() == 0 {
|
||||||
|
return ErrResourceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *RiskAssessmentBoundary) Delete(ctx context.Context, conn pg.Tx, scope Scoper, id gid.GID) error {
|
||||||
|
q := `
|
||||||
|
DELETE FROM risk_assessment_boundaries
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND id = @id
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
|
args := pgx.StrictNamedArgs{"id": id}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
|
||||||
|
_, err := conn.Exec(ctx, q, args)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
75
pkg/coredata/risk_assessment_boundary_order_field.go
Normal file
75
pkg/coredata/risk_assessment_boundary_order_field.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RiskAssessmentBoundaryOrderField string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RiskAssessmentBoundaryOrderFieldCreatedAt RiskAssessmentBoundaryOrderField = "CREATED_AT"
|
||||||
|
RiskAssessmentBoundaryOrderFieldName RiskAssessmentBoundaryOrderField = "NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ page.OrderField = RiskAssessmentBoundaryOrderField("")
|
||||||
|
_ fmt.Stringer = RiskAssessmentBoundaryOrderField("")
|
||||||
|
_ encoding.TextMarshaler = RiskAssessmentBoundaryOrderField("")
|
||||||
|
_ encoding.TextUnmarshaler = (*RiskAssessmentBoundaryOrderField)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func RiskAssessmentBoundaryOrderFields() []RiskAssessmentBoundaryOrderField {
|
||||||
|
return []RiskAssessmentBoundaryOrderField{
|
||||||
|
RiskAssessmentBoundaryOrderFieldCreatedAt,
|
||||||
|
RiskAssessmentBoundaryOrderFieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RiskAssessmentBoundaryOrderField) IsValid() bool {
|
||||||
|
switch v {
|
||||||
|
case
|
||||||
|
RiskAssessmentBoundaryOrderFieldCreatedAt,
|
||||||
|
RiskAssessmentBoundaryOrderFieldName:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RiskAssessmentBoundaryOrderField) String() string {
|
||||||
|
return string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v RiskAssessmentBoundaryOrderField) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(v.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *RiskAssessmentBoundaryOrderField) UnmarshalText(text []byte) error {
|
||||||
|
val := RiskAssessmentBoundaryOrderField(text)
|
||||||
|
if !val.IsValid() {
|
||||||
|
return fmt.Errorf("invalid RiskAssessmentBoundaryOrderField value: %q", string(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
*v = val
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p RiskAssessmentBoundaryOrderField) Column() string { return string(p) }
|
||||||
@@ -34,6 +34,7 @@ type (
|
|||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
OrganizationID gid.GID `db:"organization_id"`
|
OrganizationID gid.GID `db:"organization_id"`
|
||||||
RiskAssessmentScopeID gid.GID `db:"risk_assessment_scope_id"`
|
RiskAssessmentScopeID gid.GID `db:"risk_assessment_scope_id"`
|
||||||
|
BoundaryID *gid.GID `db:"boundary_id"`
|
||||||
NodeType RiskAssessmentNodeType `db:"node_type"`
|
NodeType RiskAssessmentNodeType `db:"node_type"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
@@ -105,6 +106,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
risk_assessment_scope_id,
|
risk_assessment_scope_id,
|
||||||
|
boundary_id,
|
||||||
node_type,
|
node_type,
|
||||||
name,
|
name,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -147,6 +149,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
risk_assessment_scope_id,
|
risk_assessment_scope_id,
|
||||||
|
boundary_id,
|
||||||
node_type,
|
node_type,
|
||||||
name,
|
name,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -212,6 +215,7 @@ SELECT
|
|||||||
id,
|
id,
|
||||||
organization_id,
|
organization_id,
|
||||||
risk_assessment_scope_id,
|
risk_assessment_scope_id,
|
||||||
|
boundary_id,
|
||||||
node_type,
|
node_type,
|
||||||
name,
|
name,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -253,6 +257,7 @@ INSERT INTO risk_assessment_nodes (
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
organization_id,
|
organization_id,
|
||||||
risk_assessment_scope_id,
|
risk_assessment_scope_id,
|
||||||
|
boundary_id,
|
||||||
node_type,
|
node_type,
|
||||||
name,
|
name,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -262,6 +267,7 @@ INSERT INTO risk_assessment_nodes (
|
|||||||
@tenant_id,
|
@tenant_id,
|
||||||
@organization_id,
|
@organization_id,
|
||||||
@risk_assessment_scope_id,
|
@risk_assessment_scope_id,
|
||||||
|
@boundary_id,
|
||||||
@node_type,
|
@node_type,
|
||||||
@name,
|
@name,
|
||||||
@created_at,
|
@created_at,
|
||||||
@@ -273,6 +279,7 @@ INSERT INTO risk_assessment_nodes (
|
|||||||
"tenant_id": scope.GetTenantID(),
|
"tenant_id": scope.GetTenantID(),
|
||||||
"organization_id": n.OrganizationID,
|
"organization_id": n.OrganizationID,
|
||||||
"risk_assessment_scope_id": n.RiskAssessmentScopeID,
|
"risk_assessment_scope_id": n.RiskAssessmentScopeID,
|
||||||
|
"boundary_id": n.BoundaryID,
|
||||||
"node_type": n.NodeType,
|
"node_type": n.NodeType,
|
||||||
"name": n.Name,
|
"name": n.Name,
|
||||||
"created_at": n.CreatedAt,
|
"created_at": n.CreatedAt,
|
||||||
@@ -295,6 +302,7 @@ func (n *RiskAssessmentNode) Update(ctx context.Context, conn pg.Tx, scope Scope
|
|||||||
q := `
|
q := `
|
||||||
UPDATE risk_assessment_nodes
|
UPDATE risk_assessment_nodes
|
||||||
SET
|
SET
|
||||||
|
boundary_id = @boundary_id,
|
||||||
node_type = @node_type,
|
node_type = @node_type,
|
||||||
name = @name,
|
name = @name,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
@@ -305,6 +313,7 @@ WHERE
|
|||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"id": n.ID,
|
"id": n.ID,
|
||||||
|
"boundary_id": n.BoundaryID,
|
||||||
"node_type": n.NodeType,
|
"node_type": n.NodeType,
|
||||||
"name": n.Name,
|
"name": n.Name,
|
||||||
"updated_at": n.UpdatedAt,
|
"updated_at": n.UpdatedAt,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ type RiskAssessmentNodeType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
RiskAssessmentNodeTypeEntity RiskAssessmentNodeType = "ENTITY"
|
RiskAssessmentNodeTypeEntity RiskAssessmentNodeType = "ENTITY"
|
||||||
RiskAssessmentNodeTypeBoundary RiskAssessmentNodeType = "BOUNDARY"
|
|
||||||
RiskAssessmentNodeTypeAsset RiskAssessmentNodeType = "ASSET"
|
RiskAssessmentNodeTypeAsset RiskAssessmentNodeType = "ASSET"
|
||||||
RiskAssessmentNodeTypeData RiskAssessmentNodeType = "DATA"
|
RiskAssessmentNodeTypeData RiskAssessmentNodeType = "DATA"
|
||||||
)
|
)
|
||||||
@@ -37,7 +36,6 @@ var (
|
|||||||
func RiskAssessmentNodeTypes() []RiskAssessmentNodeType {
|
func RiskAssessmentNodeTypes() []RiskAssessmentNodeType {
|
||||||
return []RiskAssessmentNodeType{
|
return []RiskAssessmentNodeType{
|
||||||
RiskAssessmentNodeTypeEntity,
|
RiskAssessmentNodeTypeEntity,
|
||||||
RiskAssessmentNodeTypeBoundary,
|
|
||||||
RiskAssessmentNodeTypeAsset,
|
RiskAssessmentNodeTypeAsset,
|
||||||
RiskAssessmentNodeTypeData,
|
RiskAssessmentNodeTypeData,
|
||||||
}
|
}
|
||||||
@@ -47,7 +45,6 @@ func (v RiskAssessmentNodeType) IsValid() bool {
|
|||||||
switch v {
|
switch v {
|
||||||
case
|
case
|
||||||
RiskAssessmentNodeTypeEntity,
|
RiskAssessmentNodeTypeEntity,
|
||||||
RiskAssessmentNodeTypeBoundary,
|
|
||||||
RiskAssessmentNodeTypeAsset,
|
RiskAssessmentNodeTypeAsset,
|
||||||
RiskAssessmentNodeTypeData:
|
RiskAssessmentNodeTypeData:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -424,6 +424,13 @@ const (
|
|||||||
ActionRiskAssessmentNodeUpdate = "core:risk-assessment-node:update"
|
ActionRiskAssessmentNodeUpdate = "core:risk-assessment-node:update"
|
||||||
ActionRiskAssessmentNodeDelete = "core:risk-assessment-node:delete"
|
ActionRiskAssessmentNodeDelete = "core:risk-assessment-node:delete"
|
||||||
|
|
||||||
|
// RiskAssessmentBoundary actions
|
||||||
|
ActionRiskAssessmentBoundaryGet = "core:risk-assessment-boundary:get"
|
||||||
|
ActionRiskAssessmentBoundaryList = "core:risk-assessment-boundary:list"
|
||||||
|
ActionRiskAssessmentBoundaryCreate = "core:risk-assessment-boundary:create"
|
||||||
|
ActionRiskAssessmentBoundaryUpdate = "core:risk-assessment-boundary:update"
|
||||||
|
ActionRiskAssessmentBoundaryDelete = "core:risk-assessment-boundary:delete"
|
||||||
|
|
||||||
// RiskAssessmentProcess actions
|
// RiskAssessmentProcess actions
|
||||||
ActionRiskAssessmentProcessGet = "core:risk-assessment-process:get"
|
ActionRiskAssessmentProcessGet = "core:risk-assessment-process:get"
|
||||||
ActionRiskAssessmentProcessList = "core:risk-assessment-process:list"
|
ActionRiskAssessmentProcessList = "core:risk-assessment-process:list"
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ var ViewerPolicy = policy.NewPolicy(
|
|||||||
ActionRiskAssessmentGet, ActionRiskAssessmentList,
|
ActionRiskAssessmentGet, ActionRiskAssessmentList,
|
||||||
ActionRiskAssessmentScopeGet, ActionRiskAssessmentScopeList,
|
ActionRiskAssessmentScopeGet, ActionRiskAssessmentScopeList,
|
||||||
ActionRiskAssessmentNodeGet, ActionRiskAssessmentNodeList,
|
ActionRiskAssessmentNodeGet, ActionRiskAssessmentNodeList,
|
||||||
|
ActionRiskAssessmentBoundaryGet, ActionRiskAssessmentBoundaryList,
|
||||||
ActionRiskAssessmentProcessGet, ActionRiskAssessmentProcessList,
|
ActionRiskAssessmentProcessGet, ActionRiskAssessmentProcessList,
|
||||||
ActionRiskAssessmentThreatGet, ActionRiskAssessmentThreatList,
|
ActionRiskAssessmentThreatGet, ActionRiskAssessmentThreatList,
|
||||||
ActionRiskAssessmentScenarioGet, ActionRiskAssessmentScenarioList,
|
ActionRiskAssessmentScenarioGet, ActionRiskAssessmentScenarioList,
|
||||||
@@ -169,6 +170,7 @@ var AuditorPolicy = policy.NewPolicy(
|
|||||||
ActionRiskAssessmentGet, ActionRiskAssessmentList,
|
ActionRiskAssessmentGet, ActionRiskAssessmentList,
|
||||||
ActionRiskAssessmentScopeGet, ActionRiskAssessmentScopeList,
|
ActionRiskAssessmentScopeGet, ActionRiskAssessmentScopeList,
|
||||||
ActionRiskAssessmentNodeGet, ActionRiskAssessmentNodeList,
|
ActionRiskAssessmentNodeGet, ActionRiskAssessmentNodeList,
|
||||||
|
ActionRiskAssessmentBoundaryGet, ActionRiskAssessmentBoundaryList,
|
||||||
ActionRiskAssessmentProcessGet, ActionRiskAssessmentProcessList,
|
ActionRiskAssessmentProcessGet, ActionRiskAssessmentProcessList,
|
||||||
ActionRiskAssessmentThreatGet, ActionRiskAssessmentThreatList,
|
ActionRiskAssessmentThreatGet, ActionRiskAssessmentThreatList,
|
||||||
ActionRiskAssessmentScenarioGet, ActionRiskAssessmentScenarioList,
|
ActionRiskAssessmentScenarioGet, ActionRiskAssessmentScenarioList,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (string, error) {
|
func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (string, error) {
|
||||||
var (
|
var (
|
||||||
nodes coredata.RiskAssessmentNodes
|
nodes coredata.RiskAssessmentNodes
|
||||||
|
boundaries coredata.RiskAssessmentBoundaries
|
||||||
processes coredata.RiskAssessmentProcesses
|
processes coredata.RiskAssessmentProcesses
|
||||||
threats coredata.RiskAssessmentThreats
|
threats coredata.RiskAssessmentThreats
|
||||||
)
|
)
|
||||||
@@ -36,6 +37,10 @@ func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Sco
|
|||||||
return fmt.Errorf("cannot load nodes: %w", err)
|
return fmt.Errorf("cannot load nodes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := boundaries.LoadAllByRiskAssessmentScopeID(ctx, conn, scope, scopeID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := processes.LoadAllByRiskAssessmentScopeID(ctx, conn, scope, scopeID); err != nil {
|
if err := processes.LoadAllByRiskAssessmentScopeID(ctx, conn, scope, scopeID); err != nil {
|
||||||
return fmt.Errorf("cannot load processes: %w", err)
|
return fmt.Errorf("cannot load processes: %w", err)
|
||||||
}
|
}
|
||||||
@@ -50,15 +55,16 @@ func (s *Service) BuildScopeMermaidChart(ctx context.Context, scope coredata.Sco
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildScopeMermaidChart(nodes, processes, threats), nil
|
return buildScopeMermaidChart(nodes, boundaries, processes, threats), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildScopeMermaidChart(
|
func buildScopeMermaidChart(
|
||||||
nodes coredata.RiskAssessmentNodes,
|
nodes coredata.RiskAssessmentNodes,
|
||||||
|
boundaries coredata.RiskAssessmentBoundaries,
|
||||||
processes coredata.RiskAssessmentProcesses,
|
processes coredata.RiskAssessmentProcesses,
|
||||||
threats coredata.RiskAssessmentThreats,
|
threats coredata.RiskAssessmentThreats,
|
||||||
) string {
|
) string {
|
||||||
if len(nodes) == 0 {
|
if len(nodes) == 0 && len(boundaries) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,13 +73,87 @@ func buildScopeMermaidChart(
|
|||||||
nodeAlias[n.ID] = fmt.Sprintf("n%d", i)
|
nodeAlias[n.ID] = fmt.Sprintf("n%d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundaryAlias := make(map[gid.GID]string, len(boundaries))
|
||||||
|
for i, bnd := range boundaries {
|
||||||
|
boundaryAlias[bnd.ID] = fmt.Sprintf("b%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group boundaries by their parent so nested boundaries become nested subgraphs.
|
||||||
|
childBoundaries := make(map[gid.GID]coredata.RiskAssessmentBoundaries)
|
||||||
|
|
||||||
|
var rootBoundaries coredata.RiskAssessmentBoundaries
|
||||||
|
|
||||||
|
for _, bnd := range boundaries {
|
||||||
|
if bnd.ParentBoundaryID != nil {
|
||||||
|
if _, ok := boundaryAlias[*bnd.ParentBoundaryID]; ok {
|
||||||
|
childBoundaries[*bnd.ParentBoundaryID] = append(childBoundaries[*bnd.ParentBoundaryID], bnd)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootBoundaries = append(rootBoundaries, bnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group nodes by the boundary that contains them; nodes without a
|
||||||
|
// boundary (or referencing an unknown one) are rendered at the top level.
|
||||||
|
nodesByBoundary := make(map[gid.GID]coredata.RiskAssessmentNodes)
|
||||||
|
|
||||||
|
var rootNodes coredata.RiskAssessmentNodes
|
||||||
|
|
||||||
|
for _, n := range nodes {
|
||||||
|
if n.BoundaryID != nil {
|
||||||
|
if _, ok := boundaryAlias[*n.BoundaryID]; ok {
|
||||||
|
nodesByBoundary[*n.BoundaryID] = append(nodesByBoundary[*n.BoundaryID], n)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootNodes = append(rootNodes, n)
|
||||||
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("flowchart LR\n")
|
b.WriteString("flowchart LR\n")
|
||||||
|
|
||||||
for _, n := range nodes {
|
// class statements must live at the flowchart level, not inside a
|
||||||
|
// subgraph block, so collect them and emit once all shapes are written.
|
||||||
|
var classLines []string
|
||||||
|
|
||||||
|
emitNode := func(n *coredata.RiskAssessmentNode, indent string) {
|
||||||
id := nodeAlias[n.ID]
|
id := nodeAlias[n.ID]
|
||||||
fmt.Fprintf(&b, " %s\n", mermaidNodeShape(n.NodeType, id, n.Name))
|
fmt.Fprintf(&b, "%s%s\n", indent, mermaidNodeShape(n.NodeType, id, n.Name))
|
||||||
fmt.Fprintf(&b, " class %s %s\n", id, mermaidNodeClass(n.NodeType))
|
classLines = append(classLines, fmt.Sprintf(" class %s %s", id, mermaidNodeClass(n.NodeType)))
|
||||||
|
}
|
||||||
|
|
||||||
|
var emitBoundary func(bnd *coredata.RiskAssessmentBoundary, indent string)
|
||||||
|
|
||||||
|
emitBoundary = func(bnd *coredata.RiskAssessmentBoundary, indent string) {
|
||||||
|
alias := boundaryAlias[bnd.ID]
|
||||||
|
fmt.Fprintf(&b, "%ssubgraph %s[\"%s\"]\n", indent, alias, escapeMermaidLabel(bnd.Name))
|
||||||
|
|
||||||
|
inner := indent + " "
|
||||||
|
for _, child := range childBoundaries[bnd.ID] {
|
||||||
|
emitBoundary(child, inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range nodesByBoundary[bnd.ID] {
|
||||||
|
emitNode(n, inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, "%send\n", indent)
|
||||||
|
|
||||||
|
classLines = append(classLines, fmt.Sprintf(" class %s nodeBoundary", alias))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bnd := range rootBoundaries {
|
||||||
|
emitBoundary(bnd, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range rootNodes {
|
||||||
|
emitNode(n, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range classLines {
|
||||||
|
b.WriteString(line + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range processes {
|
for _, p := range processes {
|
||||||
@@ -111,7 +191,7 @@ func buildScopeMermaidChart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(" classDef nodeEntity fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a\n")
|
b.WriteString(" classDef nodeEntity fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a\n")
|
||||||
b.WriteString(" classDef nodeBoundary fill:#fef3c7,stroke:#b45309,color:#78350f\n")
|
b.WriteString(" classDef nodeBoundary fill:#ffffff,stroke:#b45309,color:#78350f\n")
|
||||||
b.WriteString(" classDef nodeAsset fill:#e5e7eb,stroke:#374151,color:#111827\n")
|
b.WriteString(" classDef nodeAsset fill:#e5e7eb,stroke:#374151,color:#111827\n")
|
||||||
b.WriteString(" classDef nodeData fill:#dcfce7,stroke:#15803d,color:#14532d\n")
|
b.WriteString(" classDef nodeData fill:#dcfce7,stroke:#15803d,color:#14532d\n")
|
||||||
b.WriteString(" classDef nodeThreat fill:#fee2e2,stroke:#b91c1c,color:#7f1d1d\n")
|
b.WriteString(" classDef nodeThreat fill:#fee2e2,stroke:#b91c1c,color:#7f1d1d\n")
|
||||||
@@ -125,8 +205,6 @@ func mermaidNodeShape(t coredata.RiskAssessmentNodeType, id, name string) string
|
|||||||
switch t {
|
switch t {
|
||||||
case coredata.RiskAssessmentNodeTypeEntity:
|
case coredata.RiskAssessmentNodeTypeEntity:
|
||||||
return fmt.Sprintf("%s([%s])", id, label)
|
return fmt.Sprintf("%s([%s])", id, label)
|
||||||
case coredata.RiskAssessmentNodeTypeBoundary:
|
|
||||||
return fmt.Sprintf("%s{{%s}}", id, label)
|
|
||||||
case coredata.RiskAssessmentNodeTypeData:
|
case coredata.RiskAssessmentNodeTypeData:
|
||||||
return fmt.Sprintf("%s[(%s)]", id, label)
|
return fmt.Sprintf("%s[(%s)]", id, label)
|
||||||
case coredata.RiskAssessmentNodeTypeAsset:
|
case coredata.RiskAssessmentNodeTypeAsset:
|
||||||
@@ -140,8 +218,6 @@ func mermaidNodeClass(t coredata.RiskAssessmentNodeType) string {
|
|||||||
switch t {
|
switch t {
|
||||||
case coredata.RiskAssessmentNodeTypeEntity:
|
case coredata.RiskAssessmentNodeTypeEntity:
|
||||||
return "nodeEntity"
|
return "nodeEntity"
|
||||||
case coredata.RiskAssessmentNodeTypeBoundary:
|
|
||||||
return "nodeBoundary"
|
|
||||||
case coredata.RiskAssessmentNodeTypeData:
|
case coredata.RiskAssessmentNodeTypeData:
|
||||||
return "nodeData"
|
return "nodeData"
|
||||||
case coredata.RiskAssessmentNodeTypeAsset:
|
case coredata.RiskAssessmentNodeTypeAsset:
|
||||||
|
|||||||
@@ -62,14 +62,28 @@ type (
|
|||||||
Name *string
|
Name *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateRiskAssessmentBoundaryRequest struct {
|
||||||
|
RiskAssessmentScopeID gid.GID
|
||||||
|
ParentBoundaryID *gid.GID
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRiskAssessmentBoundaryRequest struct {
|
||||||
|
ID gid.GID
|
||||||
|
ParentBoundaryID **gid.GID
|
||||||
|
Name *string
|
||||||
|
}
|
||||||
|
|
||||||
CreateRiskAssessmentNodeRequest struct {
|
CreateRiskAssessmentNodeRequest struct {
|
||||||
RiskAssessmentScopeID gid.GID
|
RiskAssessmentScopeID gid.GID
|
||||||
|
BoundaryID *gid.GID
|
||||||
NodeType coredata.RiskAssessmentNodeType
|
NodeType coredata.RiskAssessmentNodeType
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateRiskAssessmentNodeRequest struct {
|
UpdateRiskAssessmentNodeRequest struct {
|
||||||
ID gid.GID
|
ID gid.GID
|
||||||
|
BoundaryID **gid.GID
|
||||||
NodeType *coredata.RiskAssessmentNodeType
|
NodeType *coredata.RiskAssessmentNodeType
|
||||||
Name *string
|
Name *string
|
||||||
}
|
}
|
||||||
@@ -169,12 +183,40 @@ func (r *UpdateRiskAssessmentScopeRequest) Validate() error {
|
|||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *CreateRiskAssessmentBoundaryRequest) Validate() error {
|
||||||
|
v := validator.New()
|
||||||
|
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
|
||||||
|
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
|
||||||
|
|
||||||
|
if r.ParentBoundaryID != nil {
|
||||||
|
v.Check(*r.ParentBoundaryID, "parent_boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UpdateRiskAssessmentBoundaryRequest) Validate() error {
|
||||||
|
v := validator.New()
|
||||||
|
v.Check(r.ID, "id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||||
|
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
||||||
|
|
||||||
|
if r.ParentBoundaryID != nil && *r.ParentBoundaryID != nil {
|
||||||
|
v.Check(**r.ParentBoundaryID, "parent_boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Error()
|
||||||
|
}
|
||||||
|
|
||||||
func (r *CreateRiskAssessmentNodeRequest) Validate() error {
|
func (r *CreateRiskAssessmentNodeRequest) Validate() error {
|
||||||
v := validator.New()
|
v := validator.New()
|
||||||
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
|
v.Check(r.RiskAssessmentScopeID, "risk_assessment_scope_id", validator.Required(), validator.GID(coredata.RiskAssessmentScopeEntityType))
|
||||||
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
|
v.Check(r.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength))
|
||||||
v.Check(r.NodeType, "node_type", validator.Required(), validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
|
v.Check(r.NodeType, "node_type", validator.Required(), validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
|
||||||
|
|
||||||
|
if r.BoundaryID != nil {
|
||||||
|
v.Check(*r.BoundaryID, "boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||||
|
}
|
||||||
|
|
||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +226,10 @@ func (r *UpdateRiskAssessmentNodeRequest) Validate() error {
|
|||||||
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
v.Check(r.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength))
|
||||||
v.Check(r.NodeType, "node_type", validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
|
v.Check(r.NodeType, "node_type", validator.OneOfSlice(coredata.RiskAssessmentNodeTypes()))
|
||||||
|
|
||||||
|
if r.BoundaryID != nil && *r.BoundaryID != nil {
|
||||||
|
v.Check(**r.BoundaryID, "boundary_id", validator.Required(), validator.GID(coredata.RiskAssessmentBoundaryEntityType))
|
||||||
|
}
|
||||||
|
|
||||||
return v.Error()
|
return v.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,6 +639,7 @@ func (s *Service) CreateNode(ctx context.Context, scope coredata.Scoper, req Cre
|
|||||||
node := &coredata.RiskAssessmentNode{
|
node := &coredata.RiskAssessmentNode{
|
||||||
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentNodeEntityType),
|
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentNodeEntityType),
|
||||||
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
|
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
|
||||||
|
BoundaryID: req.BoundaryID,
|
||||||
NodeType: req.NodeType,
|
NodeType: req.NodeType,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
@@ -607,6 +654,12 @@ func (s *Service) CreateNode(ctx context.Context, scope coredata.Scoper, req Cre
|
|||||||
return fmt.Errorf("cannot load risk assessment scope: %w", err)
|
return fmt.Errorf("cannot load risk assessment scope: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.BoundaryID != nil {
|
||||||
|
if err := s.assertBoundaryInScope(ctx, tx, scope, *req.BoundaryID, req.RiskAssessmentScopeID, "boundary_id"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
node.OrganizationID = raScope.OrganizationID
|
node.OrganizationID = raScope.OrganizationID
|
||||||
if err := node.Insert(ctx, tx, scope); err != nil {
|
if err := node.Insert(ctx, tx, scope); err != nil {
|
||||||
return fmt.Errorf("cannot insert risk assessment node: %w", err)
|
return fmt.Errorf("cannot insert risk assessment node: %w", err)
|
||||||
@@ -664,6 +717,16 @@ func (s *Service) UpdateNode(ctx context.Context, scope coredata.Scoper, req Upd
|
|||||||
node.NodeType = *req.NodeType
|
node.NodeType = *req.NodeType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.BoundaryID != nil {
|
||||||
|
if *req.BoundaryID != nil {
|
||||||
|
if err := s.assertBoundaryInScope(ctx, tx, scope, **req.BoundaryID, node.RiskAssessmentScopeID, "boundary_id"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.BoundaryID = *req.BoundaryID
|
||||||
|
}
|
||||||
|
|
||||||
node.UpdatedAt = time.Now()
|
node.UpdatedAt = time.Now()
|
||||||
if err := node.Update(ctx, tx, scope); err != nil {
|
if err := node.Update(ctx, tx, scope); err != nil {
|
||||||
return fmt.Errorf("cannot update risk assessment node: %w", err)
|
return fmt.Errorf("cannot update risk assessment node: %w", err)
|
||||||
@@ -741,6 +804,179 @@ func (s *Service) CountNodesForScopeID(ctx context.Context, scope coredata.Scope
|
|||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateBoundary(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentBoundaryRequest) (*coredata.RiskAssessmentBoundary, error) {
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
boundary := &coredata.RiskAssessmentBoundary{
|
||||||
|
ID: gid.New(scope.GetTenantID(), coredata.RiskAssessmentBoundaryEntityType),
|
||||||
|
RiskAssessmentScopeID: req.RiskAssessmentScopeID,
|
||||||
|
ParentBoundaryID: req.ParentBoundaryID,
|
||||||
|
Name: req.Name,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
raScope := coredata.RiskAssessmentScope{}
|
||||||
|
if err := raScope.LoadByID(ctx, tx, scope, req.RiskAssessmentScopeID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load risk assessment scope: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentBoundaryID != nil {
|
||||||
|
if err := s.assertBoundaryInScope(ctx, tx, scope, *req.ParentBoundaryID, req.RiskAssessmentScopeID, "parent_boundary_id"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary.OrganizationID = raScope.OrganizationID
|
||||||
|
if err := boundary.Insert(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot insert risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetBoundary(ctx context.Context, scope coredata.Scoper, id gid.GID) (*coredata.RiskAssessmentBoundary, error) {
|
||||||
|
boundary := &coredata.RiskAssessmentBoundary{}
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
if err := boundary.LoadByID(ctx, conn, scope, id); err != nil {
|
||||||
|
return fmt.Errorf("cannot load risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateBoundary(ctx context.Context, scope coredata.Scoper, req UpdateRiskAssessmentBoundaryRequest) (*coredata.RiskAssessmentBoundary, error) {
|
||||||
|
if err := req.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary := &coredata.RiskAssessmentBoundary{}
|
||||||
|
|
||||||
|
err := s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
if err := boundary.LoadByID(ctx, tx, scope, req.ID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
boundary.Name = *req.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ParentBoundaryID != nil {
|
||||||
|
if *req.ParentBoundaryID != nil {
|
||||||
|
if err := s.assertBoundaryInScope(ctx, tx, scope, **req.ParentBoundaryID, boundary.RiskAssessmentScopeID, "parent_boundary_id"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.assertNoBoundaryCycle(ctx, tx, scope, boundary.ID, **req.ParentBoundaryID, "parent_boundary_id"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary.ParentBoundaryID = *req.ParentBoundaryID
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary.UpdatedAt = time.Now()
|
||||||
|
if err := boundary.Update(ctx, tx, scope); err != nil {
|
||||||
|
return fmt.Errorf("cannot update risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return boundary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) DeleteBoundary(ctx context.Context, scope coredata.Scoper, id gid.GID) error {
|
||||||
|
return s.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
boundary := &coredata.RiskAssessmentBoundary{}
|
||||||
|
if err := boundary.Delete(ctx, tx, scope, id); err != nil {
|
||||||
|
return fmt.Errorf("cannot delete risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListBoundariesForScopeID(
|
||||||
|
ctx context.Context,
|
||||||
|
scope coredata.Scoper,
|
||||||
|
scopeID gid.GID,
|
||||||
|
cursor *page.Cursor[coredata.RiskAssessmentBoundaryOrderField],
|
||||||
|
) (*page.Page[*coredata.RiskAssessmentBoundary, coredata.RiskAssessmentBoundaryOrderField], error) {
|
||||||
|
var results coredata.RiskAssessmentBoundaries
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
if err := results.LoadByRiskAssessmentScopeID(ctx, conn, scope, scopeID, cursor); err != nil {
|
||||||
|
return fmt.Errorf("cannot list risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return page.NewPage(results, cursor), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CountBoundariesForScopeID(ctx context.Context, scope coredata.Scoper, scopeID gid.GID) (int, error) {
|
||||||
|
var count int
|
||||||
|
|
||||||
|
err := s.pg.WithConn(
|
||||||
|
ctx,
|
||||||
|
func(ctx context.Context, conn pg.Querier) (err error) {
|
||||||
|
bs := &coredata.RiskAssessmentBoundaries{}
|
||||||
|
|
||||||
|
count, err = bs.CountByRiskAssessmentScopeID(ctx, conn, scope, scopeID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count risk assessment boundaries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) CreateProcess(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentProcessRequest) (*coredata.RiskAssessmentProcess, error) {
|
func (s *Service) CreateProcess(ctx context.Context, scope coredata.Scoper, req CreateRiskAssessmentProcessRequest) (*coredata.RiskAssessmentProcess, error) {
|
||||||
if err := req.Validate(); err != nil {
|
if err := req.Validate(); err != nil {
|
||||||
return nil, fmt.Errorf("invalid request: %w", err)
|
return nil, fmt.Errorf("invalid request: %w", err)
|
||||||
@@ -1594,6 +1830,79 @@ func (s *Service) assertNodeInScope(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) assertBoundaryInScope(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
scope coredata.Scoper,
|
||||||
|
boundaryID gid.GID,
|
||||||
|
scopeID gid.GID,
|
||||||
|
field string,
|
||||||
|
) error {
|
||||||
|
boundary := &coredata.RiskAssessmentBoundary{}
|
||||||
|
if err := boundary.LoadByID(ctx, tx, scope, boundaryID); err != nil {
|
||||||
|
return validator.ValidationErrors{{
|
||||||
|
Field: field,
|
||||||
|
Code: validator.ErrorCodeCustom,
|
||||||
|
Message: "boundary not found",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A boundary in a different scope is reported identically to a missing
|
||||||
|
// one so the error does not reveal that the resource exists elsewhere.
|
||||||
|
if boundary.RiskAssessmentScopeID != scopeID {
|
||||||
|
return validator.ValidationErrors{{
|
||||||
|
Field: field,
|
||||||
|
Code: validator.ErrorCodeCustom,
|
||||||
|
Message: "boundary not found",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertNoBoundaryCycle walks the ancestor chain starting from the proposed
|
||||||
|
// parent. If it reaches the boundary being updated, the new parent would make
|
||||||
|
// the boundary an ancestor of itself (a cycle), which is rejected. A visited
|
||||||
|
// set guards against any pre-existing cycle in stored data.
|
||||||
|
func (s *Service) assertNoBoundaryCycle(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
scope coredata.Scoper,
|
||||||
|
boundaryID gid.GID,
|
||||||
|
proposedParentID gid.GID,
|
||||||
|
field string,
|
||||||
|
) error {
|
||||||
|
visited := make(map[gid.GID]bool)
|
||||||
|
currentID := proposedParentID
|
||||||
|
|
||||||
|
for {
|
||||||
|
if currentID == boundaryID {
|
||||||
|
return validator.ValidationErrors{{
|
||||||
|
Field: field,
|
||||||
|
Code: validator.ErrorCodeCustom,
|
||||||
|
Message: "boundary cannot be nested under itself or one of its descendants",
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if visited[currentID] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
visited[currentID] = true
|
||||||
|
|
||||||
|
current := &coredata.RiskAssessmentBoundary{}
|
||||||
|
if err := current.LoadByID(ctx, tx, scope, currentID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load parent boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.ParentBoundaryID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentID = *current.ParentBoundaryID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) assertProcessInScope(
|
func (s *Service) assertProcessInScope(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx pg.Tx,
|
tx pg.Tx,
|
||||||
|
|||||||
@@ -170,6 +170,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
|||||||
|
|
||||||
return types.NewRiskAssessmentScope(s), nil
|
return types.NewRiskAssessmentScope(s), nil
|
||||||
}
|
}
|
||||||
|
case coredata.RiskAssessmentBoundaryEntityType:
|
||||||
|
action = probo.ActionRiskAssessmentBoundaryGet
|
||||||
|
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
||||||
|
b, err := r.riskManagement.GetBoundary(ctx, scope, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewRiskAssessmentBoundary(b), nil
|
||||||
|
}
|
||||||
case coredata.RiskAssessmentScenarioEntityType:
|
case coredata.RiskAssessmentScenarioEntityType:
|
||||||
action = probo.ActionRiskAssessmentScenarioGet
|
action = probo.ActionRiskAssessmentScenarioGet
|
||||||
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
||||||
|
|||||||
@@ -50,10 +50,6 @@ enum RiskAssessmentNodeType
|
|||||||
@goEnum(
|
@goEnum(
|
||||||
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeTypeEntity"
|
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeTypeEntity"
|
||||||
)
|
)
|
||||||
BOUNDARY
|
|
||||||
@goEnum(
|
|
||||||
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeTypeBoundary"
|
|
||||||
)
|
|
||||||
ASSET
|
ASSET
|
||||||
@goEnum(
|
@goEnum(
|
||||||
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeTypeAsset"
|
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeTypeAsset"
|
||||||
@@ -78,6 +74,20 @@ enum RiskAssessmentNodeOrderField
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RiskAssessmentBoundaryOrderField
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/coredata.RiskAssessmentBoundaryOrderField"
|
||||||
|
) {
|
||||||
|
CREATED_AT
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentBoundaryOrderFieldCreatedAt"
|
||||||
|
)
|
||||||
|
NAME
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.RiskAssessmentBoundaryOrderFieldName"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
enum RiskAssessmentProcessOrderField
|
enum RiskAssessmentProcessOrderField
|
||||||
@goModel(
|
@goModel(
|
||||||
model: "go.probo.inc/probo/pkg/coredata.RiskAssessmentProcessOrderField"
|
model: "go.probo.inc/probo/pkg/coredata.RiskAssessmentProcessOrderField"
|
||||||
@@ -146,6 +156,14 @@ input RiskAssessmentNodeOrder
|
|||||||
field: RiskAssessmentNodeOrderField!
|
field: RiskAssessmentNodeOrderField!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input RiskAssessmentBoundaryOrder
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskAssessmentBoundaryOrderBy"
|
||||||
|
) {
|
||||||
|
direction: OrderDirection!
|
||||||
|
field: RiskAssessmentBoundaryOrderField!
|
||||||
|
}
|
||||||
|
|
||||||
input RiskAssessmentProcessOrder
|
input RiskAssessmentProcessOrder
|
||||||
@goModel(
|
@goModel(
|
||||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskAssessmentProcessOrderBy"
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskAssessmentProcessOrderBy"
|
||||||
@@ -204,6 +222,14 @@ type RiskAssessmentScope implements Node {
|
|||||||
orderBy: RiskAssessmentNodeOrder
|
orderBy: RiskAssessmentNodeOrder
|
||||||
): RiskAssessmentNodeConnection @goField(forceResolver: true)
|
): RiskAssessmentNodeConnection @goField(forceResolver: true)
|
||||||
|
|
||||||
|
boundaries(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: RiskAssessmentBoundaryOrder
|
||||||
|
): RiskAssessmentBoundaryConnection @goField(forceResolver: true)
|
||||||
|
|
||||||
processes(
|
processes(
|
||||||
first: Int
|
first: Int
|
||||||
after: CursorKey
|
after: CursorKey
|
||||||
@@ -237,12 +263,22 @@ type RiskAssessmentScope implements Node {
|
|||||||
type RiskAssessmentNode implements Node {
|
type RiskAssessmentNode implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
riskAssessmentScopeId: ID!
|
riskAssessmentScopeId: ID!
|
||||||
|
boundaryId: ID
|
||||||
nodeType: RiskAssessmentNodeType!
|
nodeType: RiskAssessmentNodeType!
|
||||||
name: String!
|
name: String!
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RiskAssessmentBoundary implements Node {
|
||||||
|
id: ID!
|
||||||
|
riskAssessmentScopeId: ID!
|
||||||
|
parentBoundaryId: ID
|
||||||
|
name: String!
|
||||||
|
createdAt: Datetime!
|
||||||
|
updatedAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
type RiskAssessmentProcess implements Node {
|
type RiskAssessmentProcess implements Node {
|
||||||
id: ID!
|
id: ID!
|
||||||
riskAssessmentScopeId: ID!
|
riskAssessmentScopeId: ID!
|
||||||
@@ -334,6 +370,20 @@ type RiskAssessmentNodeConnectionEdge {
|
|||||||
node: RiskAssessmentNode!
|
node: RiskAssessmentNode!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RiskAssessmentBoundaryConnection
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskAssessmentBoundaryConnection"
|
||||||
|
) {
|
||||||
|
totalCount: Int @goField(forceResolver: true)
|
||||||
|
edges: [RiskAssessmentBoundaryConnectionEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type RiskAssessmentBoundaryConnectionEdge {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: RiskAssessmentBoundary!
|
||||||
|
}
|
||||||
|
|
||||||
type RiskAssessmentProcessConnection
|
type RiskAssessmentProcessConnection
|
||||||
@goModel(
|
@goModel(
|
||||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskAssessmentProcessConnection"
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.RiskAssessmentProcessConnection"
|
||||||
@@ -409,6 +459,16 @@ extend type Mutation {
|
|||||||
input: DeleteRiskAssessmentNodeInput!
|
input: DeleteRiskAssessmentNodeInput!
|
||||||
): DeleteRiskAssessmentNodePayload!
|
): DeleteRiskAssessmentNodePayload!
|
||||||
|
|
||||||
|
createRiskAssessmentBoundary(
|
||||||
|
input: CreateRiskAssessmentBoundaryInput!
|
||||||
|
): CreateRiskAssessmentBoundaryPayload!
|
||||||
|
updateRiskAssessmentBoundary(
|
||||||
|
input: UpdateRiskAssessmentBoundaryInput!
|
||||||
|
): UpdateRiskAssessmentBoundaryPayload!
|
||||||
|
deleteRiskAssessmentBoundary(
|
||||||
|
input: DeleteRiskAssessmentBoundaryInput!
|
||||||
|
): DeleteRiskAssessmentBoundaryPayload!
|
||||||
|
|
||||||
createRiskAssessmentProcess(
|
createRiskAssessmentProcess(
|
||||||
input: CreateRiskAssessmentProcessInput!
|
input: CreateRiskAssessmentProcessInput!
|
||||||
): CreateRiskAssessmentProcessPayload!
|
): CreateRiskAssessmentProcessPayload!
|
||||||
@@ -512,12 +572,14 @@ type DeleteRiskAssessmentScopePayload {
|
|||||||
|
|
||||||
input CreateRiskAssessmentNodeInput {
|
input CreateRiskAssessmentNodeInput {
|
||||||
riskAssessmentScopeId: ID!
|
riskAssessmentScopeId: ID!
|
||||||
|
boundaryId: ID
|
||||||
nodeType: RiskAssessmentNodeType!
|
nodeType: RiskAssessmentNodeType!
|
||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateRiskAssessmentNodeInput {
|
input UpdateRiskAssessmentNodeInput {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
boundaryId: ID @goField(omittable: true)
|
||||||
nodeType: RiskAssessmentNodeType
|
nodeType: RiskAssessmentNodeType
|
||||||
name: String
|
name: String
|
||||||
}
|
}
|
||||||
@@ -538,6 +600,34 @@ type DeleteRiskAssessmentNodePayload {
|
|||||||
deletedRiskAssessmentNodeId: ID!
|
deletedRiskAssessmentNodeId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input CreateRiskAssessmentBoundaryInput {
|
||||||
|
riskAssessmentScopeId: ID!
|
||||||
|
parentBoundaryId: ID
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateRiskAssessmentBoundaryInput {
|
||||||
|
id: ID!
|
||||||
|
parentBoundaryId: ID @goField(omittable: true)
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input DeleteRiskAssessmentBoundaryInput {
|
||||||
|
riskAssessmentBoundaryId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRiskAssessmentBoundaryPayload {
|
||||||
|
riskAssessmentBoundaryEdge: RiskAssessmentBoundaryConnectionEdge!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRiskAssessmentBoundaryPayload {
|
||||||
|
riskAssessmentBoundary: RiskAssessmentBoundary!
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteRiskAssessmentBoundaryPayload {
|
||||||
|
deletedRiskAssessmentBoundaryId: ID!
|
||||||
|
}
|
||||||
|
|
||||||
input CreateRiskAssessmentProcessInput {
|
input CreateRiskAssessmentProcessInput {
|
||||||
riskAssessmentScopeId: ID!
|
riskAssessmentScopeId: ID!
|
||||||
sourceNodeId: ID!
|
sourceNodeId: ID!
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ func (r *mutationResolver) CreateRiskAssessmentNode(ctx context.Context, input t
|
|||||||
scope,
|
scope,
|
||||||
riskmanagement.CreateRiskAssessmentNodeRequest{
|
riskmanagement.CreateRiskAssessmentNodeRequest{
|
||||||
RiskAssessmentScopeID: input.RiskAssessmentScopeID,
|
RiskAssessmentScopeID: input.RiskAssessmentScopeID,
|
||||||
|
BoundaryID: input.BoundaryID,
|
||||||
NodeType: input.NodeType,
|
NodeType: input.NodeType,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
},
|
},
|
||||||
@@ -238,6 +239,7 @@ func (r *mutationResolver) UpdateRiskAssessmentNode(ctx context.Context, input t
|
|||||||
scope,
|
scope,
|
||||||
riskmanagement.UpdateRiskAssessmentNodeRequest{
|
riskmanagement.UpdateRiskAssessmentNodeRequest{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
|
BoundaryID: gqlutils.UnwrapOmittable(input.BoundaryID),
|
||||||
NodeType: input.NodeType,
|
NodeType: input.NodeType,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
},
|
},
|
||||||
@@ -275,6 +277,97 @@ func (r *mutationResolver) DeleteRiskAssessmentNode(ctx context.Context, input t
|
|||||||
return &types.DeleteRiskAssessmentNodePayload{DeletedRiskAssessmentNodeID: input.RiskAssessmentNodeID}, nil
|
return &types.DeleteRiskAssessmentNodePayload{DeletedRiskAssessmentNodeID: input.RiskAssessmentNodeID}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateRiskAssessmentBoundary is the resolver for the createRiskAssessmentBoundary field.
|
||||||
|
func (r *mutationResolver) CreateRiskAssessmentBoundary(ctx context.Context, input types.CreateRiskAssessmentBoundaryInput) (*types.CreateRiskAssessmentBoundaryPayload, error) {
|
||||||
|
scope, err := r.authorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentBoundaryCreate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary, err := r.riskManagement.CreateBoundary(
|
||||||
|
ctx,
|
||||||
|
scope,
|
||||||
|
riskmanagement.CreateRiskAssessmentBoundaryRequest{
|
||||||
|
RiskAssessmentScopeID: input.RiskAssessmentScopeID,
|
||||||
|
ParentBoundaryID: input.ParentBoundaryID,
|
||||||
|
Name: input.Name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
|
return nil, gqlutils.Conflict(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
|
||||||
|
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot create risk assessment boundary", log.Error(err))
|
||||||
|
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.CreateRiskAssessmentBoundaryPayload{
|
||||||
|
RiskAssessmentBoundaryEdge: &types.RiskAssessmentBoundaryConnectionEdge{
|
||||||
|
Cursor: boundary.CursorKey(coredata.RiskAssessmentBoundaryOrderFieldCreatedAt),
|
||||||
|
Node: types.NewRiskAssessmentBoundary(boundary),
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRiskAssessmentBoundary is the resolver for the updateRiskAssessmentBoundary field.
|
||||||
|
func (r *mutationResolver) UpdateRiskAssessmentBoundary(ctx context.Context, input types.UpdateRiskAssessmentBoundaryInput) (*types.UpdateRiskAssessmentBoundaryPayload, error) {
|
||||||
|
scope, err := r.authorize(ctx, input.ID, probo.ActionRiskAssessmentBoundaryUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary, err := r.riskManagement.UpdateBoundary(
|
||||||
|
ctx,
|
||||||
|
scope,
|
||||||
|
riskmanagement.UpdateRiskAssessmentBoundaryRequest{
|
||||||
|
ID: input.ID,
|
||||||
|
ParentBoundaryID: gqlutils.UnwrapOmittable(input.ParentBoundaryID),
|
||||||
|
Name: input.Name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||||
|
return nil, gqlutils.Conflict(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
|
||||||
|
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot update risk assessment boundary", log.Error(err))
|
||||||
|
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.UpdateRiskAssessmentBoundaryPayload{RiskAssessmentBoundary: types.NewRiskAssessmentBoundary(boundary)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRiskAssessmentBoundary is the resolver for the deleteRiskAssessmentBoundary field.
|
||||||
|
func (r *mutationResolver) DeleteRiskAssessmentBoundary(ctx context.Context, input types.DeleteRiskAssessmentBoundaryInput) (*types.DeleteRiskAssessmentBoundaryPayload, error) {
|
||||||
|
scope, err := r.authorize(ctx, input.RiskAssessmentBoundaryID, probo.ActionRiskAssessmentBoundaryDelete)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.riskManagement.DeleteBoundary(ctx, scope, input.RiskAssessmentBoundaryID); err != nil {
|
||||||
|
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||||
|
return nil, gqlutils.NotFound(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot delete risk assessment boundary", log.Error(err))
|
||||||
|
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.DeleteRiskAssessmentBoundaryPayload{DeletedRiskAssessmentBoundaryID: input.RiskAssessmentBoundaryID}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRiskAssessmentProcess is the resolver for the createRiskAssessmentProcess field.
|
// CreateRiskAssessmentProcess is the resolver for the createRiskAssessmentProcess field.
|
||||||
func (r *mutationResolver) CreateRiskAssessmentProcess(ctx context.Context, input types.CreateRiskAssessmentProcessInput) (*types.CreateRiskAssessmentProcessPayload, error) {
|
func (r *mutationResolver) CreateRiskAssessmentProcess(ctx context.Context, input types.CreateRiskAssessmentProcessInput) (*types.CreateRiskAssessmentProcessPayload, error) {
|
||||||
scope, err := r.authorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentProcessCreate)
|
scope, err := r.authorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentProcessCreate)
|
||||||
@@ -744,6 +837,22 @@ func (r *riskAssessmentResolver) Permission(ctx context.Context, obj *types.Risk
|
|||||||
return r.Resolver.Permission(ctx, obj, action)
|
return r.Resolver.Permission(ctx, obj, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TotalCount is the resolver for the totalCount field.
|
||||||
|
func (r *riskAssessmentBoundaryConnectionResolver) TotalCount(ctx context.Context, obj *types.RiskAssessmentBoundaryConnection) (*int, error) {
|
||||||
|
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionRiskAssessmentBoundaryList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := r.riskManagement.CountBoundariesForScopeID(ctx, scope, obj.ParentID)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot count risk assessment boundaries", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &count, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TotalCount is the resolver for the totalCount field.
|
// TotalCount is the resolver for the totalCount field.
|
||||||
func (r *riskAssessmentConnectionResolver) TotalCount(ctx context.Context, obj *types.RiskAssessmentConnection) (*int, error) {
|
func (r *riskAssessmentConnectionResolver) TotalCount(ctx context.Context, obj *types.RiskAssessmentConnection) (*int, error) {
|
||||||
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionRiskAssessmentList)
|
scope, err := r.authorize(ctx, obj.ParentID, probo.ActionRiskAssessmentList)
|
||||||
@@ -921,6 +1030,32 @@ func (r *riskAssessmentScopeResolver) Nodes(ctx context.Context, obj *types.Risk
|
|||||||
return types.NewRiskAssessmentNodeConnection(p, r, obj.ID), nil
|
return types.NewRiskAssessmentNodeConnection(p, r, obj.ID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Boundaries is the resolver for the boundaries field.
|
||||||
|
func (r *riskAssessmentScopeResolver) Boundaries(ctx context.Context, obj *types.RiskAssessmentScope, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskAssessmentBoundaryOrderBy) (*types.RiskAssessmentBoundaryConnection, error) {
|
||||||
|
scope, err := r.authorize(ctx, obj.ID, probo.ActionRiskAssessmentBoundaryList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.RiskAssessmentBoundaryOrderField]{
|
||||||
|
Field: coredata.RiskAssessmentBoundaryOrderFieldCreatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
if orderBy != nil {
|
||||||
|
pageOrderBy = page.OrderBy[coredata.RiskAssessmentBoundaryOrderField]{Field: orderBy.Field, Direction: orderBy.Direction}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
|
||||||
|
p, err := r.riskManagement.ListBoundariesForScopeID(ctx, scope, obj.ID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot list risk assessment boundaries", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.NewRiskAssessmentBoundaryConnection(p, r, obj.ID), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Processes is the resolver for the processes field.
|
// Processes is the resolver for the processes field.
|
||||||
func (r *riskAssessmentScopeResolver) Processes(ctx context.Context, obj *types.RiskAssessmentScope, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskAssessmentProcessOrderBy) (*types.RiskAssessmentProcessConnection, error) {
|
func (r *riskAssessmentScopeResolver) Processes(ctx context.Context, obj *types.RiskAssessmentScope, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskAssessmentProcessOrderBy) (*types.RiskAssessmentProcessConnection, error) {
|
||||||
scope, err := r.authorize(ctx, obj.ID, probo.ActionRiskAssessmentProcessList)
|
scope, err := r.authorize(ctx, obj.ID, probo.ActionRiskAssessmentProcessList)
|
||||||
@@ -1061,6 +1196,11 @@ func (r *riskAssessmentThreatConnectionResolver) TotalCount(ctx context.Context,
|
|||||||
// RiskAssessment returns schema.RiskAssessmentResolver implementation.
|
// RiskAssessment returns schema.RiskAssessmentResolver implementation.
|
||||||
func (r *Resolver) RiskAssessment() schema.RiskAssessmentResolver { return &riskAssessmentResolver{r} }
|
func (r *Resolver) RiskAssessment() schema.RiskAssessmentResolver { return &riskAssessmentResolver{r} }
|
||||||
|
|
||||||
|
// RiskAssessmentBoundaryConnection returns schema.RiskAssessmentBoundaryConnectionResolver implementation.
|
||||||
|
func (r *Resolver) RiskAssessmentBoundaryConnection() schema.RiskAssessmentBoundaryConnectionResolver {
|
||||||
|
return &riskAssessmentBoundaryConnectionResolver{r}
|
||||||
|
}
|
||||||
|
|
||||||
// RiskAssessmentConnection returns schema.RiskAssessmentConnectionResolver implementation.
|
// RiskAssessmentConnection returns schema.RiskAssessmentConnectionResolver implementation.
|
||||||
func (r *Resolver) RiskAssessmentConnection() schema.RiskAssessmentConnectionResolver {
|
func (r *Resolver) RiskAssessmentConnection() schema.RiskAssessmentConnectionResolver {
|
||||||
return &riskAssessmentConnectionResolver{r}
|
return &riskAssessmentConnectionResolver{r}
|
||||||
@@ -1102,6 +1242,7 @@ func (r *Resolver) RiskAssessmentThreatConnection() schema.RiskAssessmentThreatC
|
|||||||
}
|
}
|
||||||
|
|
||||||
type riskAssessmentResolver struct{ *Resolver }
|
type riskAssessmentResolver struct{ *Resolver }
|
||||||
|
type riskAssessmentBoundaryConnectionResolver struct{ *Resolver }
|
||||||
type riskAssessmentConnectionResolver struct{ *Resolver }
|
type riskAssessmentConnectionResolver struct{ *Resolver }
|
||||||
type riskAssessmentNodeConnectionResolver struct{ *Resolver }
|
type riskAssessmentNodeConnectionResolver struct{ *Resolver }
|
||||||
type riskAssessmentProcessConnectionResolver struct{ *Resolver }
|
type riskAssessmentProcessConnectionResolver struct{ *Resolver }
|
||||||
|
|||||||
65
pkg/server/api/console/v1/types/risk_assessment_boundary.go
Normal file
65
pkg/server/api/console/v1/types/risk_assessment_boundary.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
RiskAssessmentBoundaryOrderBy OrderBy[coredata.RiskAssessmentBoundaryOrderField]
|
||||||
|
|
||||||
|
RiskAssessmentBoundaryConnection struct {
|
||||||
|
TotalCount int
|
||||||
|
Edges []*RiskAssessmentBoundaryConnectionEdge
|
||||||
|
PageInfo PageInfo
|
||||||
|
Resolver any
|
||||||
|
ParentID gid.GID
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRiskAssessmentBoundaryConnection(
|
||||||
|
p *page.Page[*coredata.RiskAssessmentBoundary, coredata.RiskAssessmentBoundaryOrderField],
|
||||||
|
parentType any,
|
||||||
|
parentID gid.GID,
|
||||||
|
) *RiskAssessmentBoundaryConnection {
|
||||||
|
edges := make([]*RiskAssessmentBoundaryConnectionEdge, len(p.Data))
|
||||||
|
for i := range edges {
|
||||||
|
edges[i] = &RiskAssessmentBoundaryConnectionEdge{
|
||||||
|
Cursor: p.Data[i].CursorKey(p.Cursor.OrderBy.Field),
|
||||||
|
Node: NewRiskAssessmentBoundary(p.Data[i]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &RiskAssessmentBoundaryConnection{
|
||||||
|
Edges: edges,
|
||||||
|
PageInfo: *NewPageInfo(p),
|
||||||
|
Resolver: parentType,
|
||||||
|
ParentID: parentID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRiskAssessmentBoundary(b *coredata.RiskAssessmentBoundary) *RiskAssessmentBoundary {
|
||||||
|
return &RiskAssessmentBoundary{
|
||||||
|
ID: b.ID,
|
||||||
|
RiskAssessmentScopeID: b.RiskAssessmentScopeID,
|
||||||
|
ParentBoundaryID: b.ParentBoundaryID,
|
||||||
|
Name: b.Name,
|
||||||
|
CreatedAt: b.CreatedAt,
|
||||||
|
UpdatedAt: b.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ func NewRiskAssessmentNode(n *coredata.RiskAssessmentNode) *RiskAssessmentNode {
|
|||||||
return &RiskAssessmentNode{
|
return &RiskAssessmentNode{
|
||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
RiskAssessmentScopeID: n.RiskAssessmentScopeID,
|
RiskAssessmentScopeID: n.RiskAssessmentScopeID,
|
||||||
|
BoundaryID: n.BoundaryID,
|
||||||
NodeType: n.NodeType,
|
NodeType: n.NodeType,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
CreatedAt: n.CreatedAt,
|
CreatedAt: n.CreatedAt,
|
||||||
|
|||||||
@@ -6497,6 +6497,7 @@ func (r *Resolver) AddRiskAssessmentNodeTool(ctx context.Context, req *mcp.CallT
|
|||||||
|
|
||||||
n, err := r.riskManagement.CreateNode(ctx, scope, riskmanagement.CreateRiskAssessmentNodeRequest{
|
n, err := r.riskManagement.CreateNode(ctx, scope, riskmanagement.CreateRiskAssessmentNodeRequest{
|
||||||
RiskAssessmentScopeID: input.RiskAssessmentScopeID,
|
RiskAssessmentScopeID: input.RiskAssessmentScopeID,
|
||||||
|
BoundaryID: input.BoundaryID,
|
||||||
NodeType: input.NodeType,
|
NodeType: input.NodeType,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
})
|
})
|
||||||
@@ -6515,8 +6516,14 @@ func (r *Resolver) UpdateRiskAssessmentNodeTool(ctx context.Context, req *mcp.Ca
|
|||||||
return nil, types.UpdateRiskAssessmentNodeOutput{}, err
|
return nil, types.UpdateRiskAssessmentNodeOutput{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var boundaryID **gid.GID
|
||||||
|
if input.BoundaryID != nil {
|
||||||
|
boundaryID = &input.BoundaryID
|
||||||
|
}
|
||||||
|
|
||||||
n, err := r.riskManagement.UpdateNode(ctx, scope, riskmanagement.UpdateRiskAssessmentNodeRequest{
|
n, err := r.riskManagement.UpdateNode(ctx, scope, riskmanagement.UpdateRiskAssessmentNodeRequest{
|
||||||
ID: input.ID,
|
ID: input.ID,
|
||||||
|
BoundaryID: boundaryID,
|
||||||
NodeType: input.NodeType,
|
NodeType: input.NodeType,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
})
|
})
|
||||||
@@ -6930,3 +6937,106 @@ func (r *Resolver) GetRiskAssessmentScopeMermaidChartTool(ctx context.Context, r
|
|||||||
MermaidChart: chart,
|
MermaidChart: chart,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) ListRiskAssessmentBoundariesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRiskAssessmentBoundariesInput) (*mcp.CallToolResult, types.ListRiskAssessmentBoundariesOutput, error) {
|
||||||
|
scope, err := r.Authorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentBoundaryList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.ListRiskAssessmentBoundariesOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.RiskAssessmentBoundaryOrderField]{
|
||||||
|
Field: coredata.RiskAssessmentBoundaryOrderFieldCreatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
if input.OrderBy != nil {
|
||||||
|
pageOrderBy = page.OrderBy[coredata.RiskAssessmentBoundaryOrderField]{
|
||||||
|
Field: input.OrderBy.Field,
|
||||||
|
Direction: input.OrderBy.Direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||||
|
|
||||||
|
p, err := r.riskManagement.ListBoundariesForScopeID(ctx, scope, input.RiskAssessmentScopeID, cursor)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("cannot list risk assessment boundaries: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.NewListRiskAssessmentBoundariesOutput(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) GetRiskAssessmentBoundaryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskAssessmentBoundaryInput) (*mcp.CallToolResult, types.GetRiskAssessmentBoundaryOutput, error) {
|
||||||
|
scope, err := r.Authorize(ctx, input.ID, probo.ActionRiskAssessmentBoundaryGet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.GetRiskAssessmentBoundaryOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := r.riskManagement.GetBoundary(ctx, scope, input.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.GetRiskAssessmentBoundaryOutput{}, fmt.Errorf("failed to get risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.GetRiskAssessmentBoundaryOutput{
|
||||||
|
RiskAssessmentBoundary: types.NewRiskAssessmentBoundary(b),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) AddRiskAssessmentBoundaryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskAssessmentBoundaryInput) (*mcp.CallToolResult, types.AddRiskAssessmentBoundaryOutput, error) {
|
||||||
|
scope, err := r.Authorize(ctx, input.RiskAssessmentScopeID, probo.ActionRiskAssessmentBoundaryCreate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.AddRiskAssessmentBoundaryOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := r.riskManagement.CreateBoundary(ctx, scope, riskmanagement.CreateRiskAssessmentBoundaryRequest{
|
||||||
|
RiskAssessmentScopeID: input.RiskAssessmentScopeID,
|
||||||
|
ParentBoundaryID: input.ParentBoundaryID,
|
||||||
|
Name: input.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.AddRiskAssessmentBoundaryOutput{}, fmt.Errorf("failed to create risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.AddRiskAssessmentBoundaryOutput{
|
||||||
|
RiskAssessmentBoundary: types.NewRiskAssessmentBoundary(b),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) UpdateRiskAssessmentBoundaryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskAssessmentBoundaryInput) (*mcp.CallToolResult, types.UpdateRiskAssessmentBoundaryOutput, error) {
|
||||||
|
scope, err := r.Authorize(ctx, input.ID, probo.ActionRiskAssessmentBoundaryUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.UpdateRiskAssessmentBoundaryOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentBoundaryID **gid.GID
|
||||||
|
if input.ParentBoundaryID != nil {
|
||||||
|
parentBoundaryID = &input.ParentBoundaryID
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := r.riskManagement.UpdateBoundary(ctx, scope, riskmanagement.UpdateRiskAssessmentBoundaryRequest{
|
||||||
|
ID: input.ID,
|
||||||
|
ParentBoundaryID: parentBoundaryID,
|
||||||
|
Name: input.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.UpdateRiskAssessmentBoundaryOutput{}, fmt.Errorf("failed to update risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.UpdateRiskAssessmentBoundaryOutput{
|
||||||
|
RiskAssessmentBoundary: types.NewRiskAssessmentBoundary(b),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resolver) DeleteRiskAssessmentBoundaryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRiskAssessmentBoundaryInput) (*mcp.CallToolResult, types.DeleteRiskAssessmentBoundaryOutput, error) {
|
||||||
|
scope, err := r.Authorize(ctx, input.ID, probo.ActionRiskAssessmentBoundaryDelete)
|
||||||
|
if err != nil {
|
||||||
|
return nil, types.DeleteRiskAssessmentBoundaryOutput{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.riskManagement.DeleteBoundary(ctx, scope, input.ID); err != nil {
|
||||||
|
return nil, types.DeleteRiskAssessmentBoundaryOutput{}, fmt.Errorf("failed to delete risk assessment boundary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, types.DeleteRiskAssessmentBoundaryOutput{
|
||||||
|
DeletedRiskAssessmentBoundaryID: input.ID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10792,7 +10792,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- ENTITY
|
- ENTITY
|
||||||
- BOUNDARY
|
|
||||||
- ASSET
|
- ASSET
|
||||||
- DATA
|
- DATA
|
||||||
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeType
|
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentNodeType
|
||||||
@@ -10851,6 +10850,24 @@ components:
|
|||||||
direction:
|
direction:
|
||||||
$ref: "#/components/schemas/OrderDirection"
|
$ref: "#/components/schemas/OrderDirection"
|
||||||
|
|
||||||
|
RiskAssessmentBoundaryOrderField:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- CREATED_AT
|
||||||
|
- NAME
|
||||||
|
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RiskAssessmentBoundaryOrderField
|
||||||
|
|
||||||
|
RiskAssessmentBoundaryOrderBy:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- field
|
||||||
|
- direction
|
||||||
|
properties:
|
||||||
|
field:
|
||||||
|
$ref: "#/components/schemas/RiskAssessmentBoundaryOrderField"
|
||||||
|
direction:
|
||||||
|
$ref: "#/components/schemas/OrderDirection"
|
||||||
|
|
||||||
RiskAssessmentProcessOrderField:
|
RiskAssessmentProcessOrderField:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
@@ -10973,6 +10990,9 @@ components:
|
|||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
risk_assessment_scope_id:
|
risk_assessment_scope_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
|
boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: ID of the boundary that contains this node, if any
|
||||||
node_type:
|
node_type:
|
||||||
$ref: "#/components/schemas/RiskAssessmentNodeType"
|
$ref: "#/components/schemas/RiskAssessmentNodeType"
|
||||||
name:
|
name:
|
||||||
@@ -10984,6 +11004,34 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
|
|
||||||
|
RiskAssessmentBoundary:
|
||||||
|
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"
|
||||||
|
parent_boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: ID of the parent boundary, if this boundary is nested
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
updated_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
|
||||||
RiskAssessmentProcess:
|
RiskAssessmentProcess:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -11372,6 +11420,9 @@ components:
|
|||||||
risk_assessment_scope_id:
|
risk_assessment_scope_id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Risk assessment scope ID
|
description: Risk assessment scope ID
|
||||||
|
boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: ID of the boundary that contains this node (optional)
|
||||||
node_type:
|
node_type:
|
||||||
$ref: "#/components/schemas/RiskAssessmentNodeType"
|
$ref: "#/components/schemas/RiskAssessmentNodeType"
|
||||||
description: Node type
|
description: Node type
|
||||||
@@ -11395,6 +11446,9 @@ components:
|
|||||||
id:
|
id:
|
||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Risk assessment node ID
|
description: Risk assessment node ID
|
||||||
|
boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: ID of the boundary that contains this node (optional)
|
||||||
node_type:
|
node_type:
|
||||||
$ref: "#/components/schemas/RiskAssessmentNodeType"
|
$ref: "#/components/schemas/RiskAssessmentNodeType"
|
||||||
description: Node type
|
description: Node type
|
||||||
@@ -11428,6 +11482,119 @@ components:
|
|||||||
$ref: "#/components/schemas/GID"
|
$ref: "#/components/schemas/GID"
|
||||||
description: Deleted risk assessment node ID
|
description: Deleted risk assessment node ID
|
||||||
|
|
||||||
|
ListRiskAssessmentBoundariesInput:
|
||||||
|
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/RiskAssessmentBoundaryOrderBy"
|
||||||
|
description: Order by
|
||||||
|
size:
|
||||||
|
type: integer
|
||||||
|
description: Page size
|
||||||
|
cursor:
|
||||||
|
$ref: "#/components/schemas/CursorKey"
|
||||||
|
description: Page cursor
|
||||||
|
|
||||||
|
ListRiskAssessmentBoundariesOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- risk_assessment_boundaries
|
||||||
|
properties:
|
||||||
|
next_cursor:
|
||||||
|
$ref: "#/components/schemas/CursorKey"
|
||||||
|
description: Next cursor
|
||||||
|
risk_assessment_boundaries:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/RiskAssessmentBoundary"
|
||||||
|
|
||||||
|
GetRiskAssessmentBoundaryInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Risk assessment boundary ID
|
||||||
|
|
||||||
|
GetRiskAssessmentBoundaryOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- risk_assessment_boundary
|
||||||
|
properties:
|
||||||
|
risk_assessment_boundary:
|
||||||
|
$ref: "#/components/schemas/RiskAssessmentBoundary"
|
||||||
|
|
||||||
|
AddRiskAssessmentBoundaryInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- risk_assessment_scope_id
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
risk_assessment_scope_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Risk assessment scope ID
|
||||||
|
parent_boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: ID of the parent boundary (optional, for nested boundaries)
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Risk assessment boundary name
|
||||||
|
|
||||||
|
AddRiskAssessmentBoundaryOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- risk_assessment_boundary
|
||||||
|
properties:
|
||||||
|
risk_assessment_boundary:
|
||||||
|
$ref: "#/components/schemas/RiskAssessmentBoundary"
|
||||||
|
|
||||||
|
UpdateRiskAssessmentBoundaryInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Risk assessment boundary ID
|
||||||
|
parent_boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: ID of the parent boundary (optional, for nested boundaries)
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Risk assessment boundary name
|
||||||
|
|
||||||
|
UpdateRiskAssessmentBoundaryOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- risk_assessment_boundary
|
||||||
|
properties:
|
||||||
|
risk_assessment_boundary:
|
||||||
|
$ref: "#/components/schemas/RiskAssessmentBoundary"
|
||||||
|
|
||||||
|
DeleteRiskAssessmentBoundaryInput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Risk assessment boundary ID
|
||||||
|
|
||||||
|
DeleteRiskAssessmentBoundaryOutput:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- deleted_risk_assessment_boundary_id
|
||||||
|
properties:
|
||||||
|
deleted_risk_assessment_boundary_id:
|
||||||
|
$ref: "#/components/schemas/GID"
|
||||||
|
description: Deleted risk assessment boundary ID
|
||||||
|
|
||||||
ListRiskAssessmentProcessesInput:
|
ListRiskAssessmentProcessesInput:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -14005,6 +14172,49 @@ tools:
|
|||||||
$ref: "#/components/schemas/DeleteRiskAssessmentNodeInput"
|
$ref: "#/components/schemas/DeleteRiskAssessmentNodeInput"
|
||||||
outputSchema:
|
outputSchema:
|
||||||
$ref: "#/components/schemas/DeleteRiskAssessmentNodeOutput"
|
$ref: "#/components/schemas/DeleteRiskAssessmentNodeOutput"
|
||||||
|
- name: listRiskAssessmentBoundaries
|
||||||
|
description: List all boundaries for a risk assessment scope
|
||||||
|
hints:
|
||||||
|
readonly: true
|
||||||
|
idempotent: true
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/ListRiskAssessmentBoundariesInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/ListRiskAssessmentBoundariesOutput"
|
||||||
|
- name: getRiskAssessmentBoundary
|
||||||
|
description: Get a risk assessment boundary by ID
|
||||||
|
hints:
|
||||||
|
readonly: true
|
||||||
|
idempotent: true
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/GetRiskAssessmentBoundaryInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/GetRiskAssessmentBoundaryOutput"
|
||||||
|
- name: addRiskAssessmentBoundary
|
||||||
|
description: Create a new risk assessment boundary
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/AddRiskAssessmentBoundaryInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/AddRiskAssessmentBoundaryOutput"
|
||||||
|
- name: updateRiskAssessmentBoundary
|
||||||
|
description: Update an existing risk assessment boundary
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/UpdateRiskAssessmentBoundaryInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/UpdateRiskAssessmentBoundaryOutput"
|
||||||
|
- name: deleteRiskAssessmentBoundary
|
||||||
|
description: Delete a risk assessment boundary
|
||||||
|
hints:
|
||||||
|
readonly: false
|
||||||
|
destructive: true
|
||||||
|
inputSchema:
|
||||||
|
$ref: "#/components/schemas/DeleteRiskAssessmentBoundaryInput"
|
||||||
|
outputSchema:
|
||||||
|
$ref: "#/components/schemas/DeleteRiskAssessmentBoundaryOutput"
|
||||||
- name: listRiskAssessmentProcesses
|
- name: listRiskAssessmentProcesses
|
||||||
description: List all processes for a risk assessment scope
|
description: List all processes for a risk assessment scope
|
||||||
hints:
|
hints:
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func NewRiskAssessmentNode(n *coredata.RiskAssessmentNode) *RiskAssessmentNode {
|
|||||||
ID: n.ID,
|
ID: n.ID,
|
||||||
OrganizationID: n.OrganizationID,
|
OrganizationID: n.OrganizationID,
|
||||||
RiskAssessmentScopeID: n.RiskAssessmentScopeID,
|
RiskAssessmentScopeID: n.RiskAssessmentScopeID,
|
||||||
|
BoundaryID: n.BoundaryID,
|
||||||
NodeType: n.NodeType,
|
NodeType: n.NodeType,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
CreatedAt: n.CreatedAt,
|
CreatedAt: n.CreatedAt,
|
||||||
@@ -95,6 +96,39 @@ func NewRiskAssessmentNode(n *coredata.RiskAssessmentNode) *RiskAssessmentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewRiskAssessmentBoundary(b *coredata.RiskAssessmentBoundary) *RiskAssessmentBoundary {
|
||||||
|
return &RiskAssessmentBoundary{
|
||||||
|
ID: b.ID,
|
||||||
|
OrganizationID: b.OrganizationID,
|
||||||
|
RiskAssessmentScopeID: b.RiskAssessmentScopeID,
|
||||||
|
ParentBoundaryID: b.ParentBoundaryID,
|
||||||
|
Name: b.Name,
|
||||||
|
CreatedAt: b.CreatedAt,
|
||||||
|
UpdatedAt: b.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListRiskAssessmentBoundariesOutput(
|
||||||
|
p *page.Page[*coredata.RiskAssessmentBoundary, coredata.RiskAssessmentBoundaryOrderField],
|
||||||
|
) ListRiskAssessmentBoundariesOutput {
|
||||||
|
items := make([]*RiskAssessmentBoundary, 0, len(p.Data))
|
||||||
|
for _, v := range p.Data {
|
||||||
|
items = append(items, NewRiskAssessmentBoundary(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 ListRiskAssessmentBoundariesOutput{
|
||||||
|
NextCursor: nextCursor,
|
||||||
|
RiskAssessmentBoundaries: items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewListRiskAssessmentNodesOutput(
|
func NewListRiskAssessmentNodesOutput(
|
||||||
p *page.Page[*coredata.RiskAssessmentNode, coredata.RiskAssessmentNodeOrderField],
|
p *page.Page[*coredata.RiskAssessmentNode, coredata.RiskAssessmentNodeOrderField],
|
||||||
) ListRiskAssessmentNodesOutput {
|
) ListRiskAssessmentNodesOutput {
|
||||||
|
|||||||
Reference in New Issue
Block a user