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:
@@ -74,3 +74,13 @@ func ValidateEnum(flag string, value string, allowed []string) error {
|
||||
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
|
||||
}
|
||||
@@ -58,9 +58,10 @@ type createResponse struct {
|
||||
|
||||
func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagScopeId string
|
||||
flagNodeType string
|
||||
flagName string
|
||||
flagScopeId string
|
||||
flagBoundaryId string
|
||||
flagNodeType string
|
||||
flagName string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -106,7 +107,6 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||
Title("Node type").
|
||||
Options(
|
||||
huh.NewOption("Entity", "ENTITY"),
|
||||
huh.NewOption("Boundary", "BOUNDARY"),
|
||||
huh.NewOption("Asset", "ASSET"),
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -136,6 +136,10 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
|
||||
"name": flagName,
|
||||
}
|
||||
|
||||
if flagBoundaryId != "" {
|
||||
input["boundaryId"] = flagBoundaryId
|
||||
}
|
||||
|
||||
data, err := client.Do(
|
||||
createMutation,
|
||||
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(&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.MarkFlagRequired("scope-id")
|
||||
|
||||
@@ -51,8 +51,10 @@ type updateResponse struct {
|
||||
|
||||
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagName string
|
||||
flagNodeType string
|
||||
flagName string
|
||||
flagNodeType string
|
||||
flagBoundaryId string
|
||||
flagClearBoundary bool
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -78,6 +80,10 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
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{
|
||||
"id": args[0],
|
||||
}
|
||||
@@ -87,13 +93,21 @@ func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
input["nodeType"] = flagNodeType
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("boundary-id") {
|
||||
input["boundaryId"] = flagBoundaryId
|
||||
}
|
||||
|
||||
if flagClearBoundary {
|
||||
input["boundaryId"] = nil
|
||||
}
|
||||
|
||||
if len(input) == 1 {
|
||||
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(&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
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package riskassessment
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"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/delete"
|
||||
"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(scope.NewCmdScope(f))
|
||||
cmd.AddCommand(node.NewCmdNode(f))
|
||||
cmd.AddCommand(boundary.NewCmdBoundary(f))
|
||||
cmd.AddCommand(process.NewCmdProcess(f))
|
||||
cmd.AddCommand(threat.NewCmdThreat(f))
|
||||
cmd.AddCommand(scenario.NewCmdScenario(f))
|
||||
|
||||
Reference in New Issue
Block a user