// Copyright (c) 2026 Probo Inc . // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY // AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. package console_test import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.probo.inc/probo/e2e/internal/factory" "go.probo.inc/probo/e2e/internal/testutil" ) func TestRiskAssessment_Create(t *testing.T) { t.Parallel() t.Run("with required fields", func(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) var result struct { CreateRiskAssessment struct { RiskAssessmentEdge struct { Node struct { ID string `json:"id"` Name string `json:"name"` } `json:"node"` } `json:"riskAssessmentEdge"` } `json:"createRiskAssessment"` } err := owner.Execute(` mutation($input: CreateRiskAssessmentInput!) { createRiskAssessment(input: $input) { riskAssessmentEdge { node { id name } } } } `, map[string]any{ "input": map[string]any{ "organizationId": owner.GetOrganizationID().String(), "name": "Platform Threat Model", }, }, &result) require.NoError(t, err) assert.NotEmpty(t, result.CreateRiskAssessment.RiskAssessmentEdge.Node.ID) assert.Equal(t, "Platform Threat Model", result.CreateRiskAssessment.RiskAssessmentEdge.Node.Name) }) } func TestRiskAssessment_Delete(t *testing.T) { t.Parallel() t.Run("cascades to scopes", func(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) _, err := owner.Do(` mutation($input: DeleteRiskAssessmentInput!) { deleteRiskAssessment(input: $input) { deletedRiskAssessmentId } } `, map[string]any{"input": map[string]any{"riskAssessmentId": raID}}) require.NoError(t, err) var result struct { Node *struct { ID string `json:"id"` } `json:"node"` } err = owner.Execute(`query($id: ID!) { node(id: $id) { ... on RiskAssessmentScope { id } } }`, map[string]any{"id": scopeID}, &result) testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "RiskAssessmentScope") }) } func TestRiskAssessmentScope_CRUD(t *testing.T) { t.Parallel() t.Run("create and list via assessment", func(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) factory.CreateRiskAssessmentScope(owner, raID, factory.Attrs{"name": "API scope"}) factory.CreateRiskAssessmentScope(owner, raID, factory.Attrs{"name": "Infra scope"}) var result struct { Node struct { Scopes struct { TotalCount int `json:"totalCount"` Edges []struct { Node struct { ID string `json:"id"` Name string `json:"name"` } `json:"node"` } `json:"edges"` } `json:"scopes"` } `json:"node"` } err := owner.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessment { scopes(first: 10) { totalCount edges { node { id name } } } } } } `, map[string]any{"id": raID}, &result) require.NoError(t, err) assert.Equal(t, 2, result.Node.Scopes.TotalCount) assert.Len(t, result.Node.Scopes.Edges, 2) }) } func TestRiskAssessmentNode_Create(t *testing.T) { t.Parallel() for _, nodeType := range []string{"ENTITY", "BOUNDARY", "ASSET", "DATA"} { t.Run("nodeType="+nodeType, func(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) var result struct { CreateRiskAssessmentNode struct { RiskAssessmentNodeEdge struct { Node struct { ID string `json:"id"` NodeType string `json:"nodeType"` } `json:"node"` } `json:"riskAssessmentNodeEdge"` } `json:"createRiskAssessmentNode"` } err := owner.Execute(` mutation($input: CreateRiskAssessmentNodeInput!) { createRiskAssessmentNode(input: $input) { riskAssessmentNodeEdge { node { id nodeType } } } } `, map[string]any{ "input": map[string]any{ "riskAssessmentScopeId": scopeID, "nodeType": nodeType, "name": "Node-" + nodeType, }, }, &result) require.NoError(t, err) assert.Equal(t, nodeType, result.CreateRiskAssessmentNode.RiskAssessmentNodeEdge.Node.NodeType) }) } } func TestRiskAssessmentProcess_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) src := factory.CreateRiskAssessmentNode(owner, scopeID, factory.Attrs{"nodeType": "ENTITY"}) dst := factory.CreateRiskAssessmentNode(owner, scopeID, factory.Attrs{"nodeType": "ASSET"}) var result struct { CreateRiskAssessmentProcess struct { RiskAssessmentProcessEdge struct { Node struct { ID string `json:"id"` SourceNodeID string `json:"sourceNodeId"` TargetNodeID string `json:"targetNodeId"` Name string `json:"name"` } `json:"node"` } `json:"riskAssessmentProcessEdge"` } `json:"createRiskAssessmentProcess"` } err := owner.Execute(` mutation($input: CreateRiskAssessmentProcessInput!) { createRiskAssessmentProcess(input: $input) { riskAssessmentProcessEdge { node { id sourceNodeId targetNodeId name } } } } `, map[string]any{ "input": map[string]any{ "riskAssessmentScopeId": scopeID, "sourceNodeId": src, "targetNodeId": dst, "name": "User → API", }, }, &result) require.NoError(t, err) assert.Equal(t, src, result.CreateRiskAssessmentProcess.RiskAssessmentProcessEdge.Node.SourceNodeID) assert.Equal(t, dst, result.CreateRiskAssessmentProcess.RiskAssessmentProcessEdge.Node.TargetNodeID) } func TestRiskAssessmentThreat_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) src := factory.CreateRiskAssessmentNode(owner, scopeID) dst := factory.CreateRiskAssessmentNode(owner, scopeID) processID := factory.CreateRiskAssessmentProcess(owner, scopeID, src, dst) var result struct { CreateRiskAssessmentThreat struct { RiskAssessmentThreatEdge struct { Node struct { ID string `json:"id"` ProcessID string `json:"processId"` Category string `json:"category"` } `json:"node"` } `json:"riskAssessmentThreatEdge"` } `json:"createRiskAssessmentThreat"` } err := owner.Execute(` mutation($input: CreateRiskAssessmentThreatInput!) { createRiskAssessmentThreat(input: $input) { riskAssessmentThreatEdge { node { id processId category } } } } `, map[string]any{ "input": map[string]any{ "riskAssessmentScopeId": scopeID, "processId": processID, "name": "SQL injection", "category": "Confidentiality", }, }, &result) require.NoError(t, err) assert.Equal(t, processID, result.CreateRiskAssessmentThreat.RiskAssessmentThreatEdge.Node.ProcessID) assert.Equal(t, "Confidentiality", result.CreateRiskAssessmentThreat.RiskAssessmentThreatEdge.Node.Category) } func TestRiskAssessmentScenario_Create(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) var result struct { CreateRiskAssessmentScenario struct { RiskAssessmentScenarioEdge struct { Node struct { ID string `json:"id"` Name string `json:"name"` } `json:"node"` } `json:"riskAssessmentScenarioEdge"` } `json:"createRiskAssessmentScenario"` } err := owner.Execute(` mutation($input: CreateRiskAssessmentScenarioInput!) { createRiskAssessmentScenario(input: $input) { riskAssessmentScenarioEdge { node { id name } } } } `, map[string]any{ "input": map[string]any{ "riskAssessmentScopeId": scopeID, "name": "SQL injection impacts data breach risk", }, }, &result) require.NoError(t, err) assert.NotEmpty(t, result.CreateRiskAssessmentScenario.RiskAssessmentScenarioEdge.Node.ID) assert.Equal(t, "SQL injection impacts data breach risk", result.CreateRiskAssessmentScenario.RiskAssessmentScenarioEdge.Node.Name) } func TestRiskAssessmentScenario_ListViaRisk(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) riskID := factory.CreateRisk(owner) s1 := factory.CreateRiskAssessmentScenario(owner, scopeID, factory.Attrs{"name": "S1"}) s2 := factory.CreateRiskAssessmentScenario(owner, scopeID, factory.Attrs{"name": "S2"}) factory.LinkRiskAssessmentScenarioRisk(owner, s1, riskID) factory.LinkRiskAssessmentScenarioRisk(owner, s2, riskID) var result struct { Node struct { Scenarios struct { TotalCount int `json:"totalCount"` Edges []struct { Node struct { ID string `json:"id"` Name string `json:"name"` } `json:"node"` } `json:"edges"` } `json:"scenarios"` } `json:"node"` } err := owner.Execute(` query($id: ID!) { node(id: $id) { ... on Risk { scenarios(first: 10) { totalCount edges { node { id name } } } } } } `, map[string]any{"id": riskID}, &result) require.NoError(t, err) assert.Equal(t, 2, result.Node.Scenarios.TotalCount) assert.Len(t, result.Node.Scenarios.Edges, 2) } func TestRiskAssessmentScenario_ListViaScope(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) factory.CreateRiskAssessmentScenario(owner, scopeID, factory.Attrs{"name": "Scenario A"}) factory.CreateRiskAssessmentScenario(owner, scopeID, factory.Attrs{"name": "Scenario B"}) var result struct { Node struct { Scenarios struct { TotalCount int `json:"totalCount"` Edges []struct { Node struct { ID string `json:"id"` Name string `json:"name"` } `json:"node"` } `json:"edges"` } `json:"scenarios"` } `json:"node"` } err := owner.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessmentScope { scenarios(first: 10) { totalCount edges { node { id name } } } } } } `, map[string]any{"id": scopeID}, &result) require.NoError(t, err) assert.Equal(t, 2, result.Node.Scenarios.TotalCount) assert.Len(t, result.Node.Scenarios.Edges, 2) } func TestRiskAssessment_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner, factory.Attrs{"name": "Original"}) var result struct { UpdateRiskAssessment struct { RiskAssessment struct { ID string `json:"id"` Name string `json:"name"` Description *string `json:"description"` } `json:"riskAssessment"` } `json:"updateRiskAssessment"` } err := owner.Execute(` mutation($input: UpdateRiskAssessmentInput!) { updateRiskAssessment(input: $input) { riskAssessment { id name description } } } `, map[string]any{ "input": map[string]any{ "id": raID, "name": "Updated", "description": "New description", }, }, &result) require.NoError(t, err) assert.Equal(t, "Updated", result.UpdateRiskAssessment.RiskAssessment.Name) require.NotNil(t, result.UpdateRiskAssessment.RiskAssessment.Description) assert.Equal(t, "New description", *result.UpdateRiskAssessment.RiskAssessment.Description) } func TestRiskAssessmentScope_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID, factory.Attrs{"name": "Original"}) var result struct { UpdateRiskAssessmentScope struct { RiskAssessmentScope struct { ID string `json:"id"` Name string `json:"name"` } `json:"riskAssessmentScope"` } `json:"updateRiskAssessmentScope"` } err := owner.Execute(` mutation($input: UpdateRiskAssessmentScopeInput!) { updateRiskAssessmentScope(input: $input) { riskAssessmentScope { id name } } } `, map[string]any{ "input": map[string]any{ "id": scopeID, "name": "Updated scope", }, }, &result) require.NoError(t, err) assert.Equal(t, "Updated scope", result.UpdateRiskAssessmentScope.RiskAssessmentScope.Name) } func TestRiskAssessmentNode_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) nodeID := factory.CreateRiskAssessmentNode(owner, scopeID, factory.Attrs{"nodeType": "ENTITY", "name": "Original"}) var result struct { UpdateRiskAssessmentNode struct { RiskAssessmentNode struct { ID string `json:"id"` Name string `json:"name"` NodeType string `json:"nodeType"` } `json:"riskAssessmentNode"` } `json:"updateRiskAssessmentNode"` } err := owner.Execute(` mutation($input: UpdateRiskAssessmentNodeInput!) { updateRiskAssessmentNode(input: $input) { riskAssessmentNode { id name nodeType } } } `, map[string]any{ "input": map[string]any{ "id": nodeID, "name": "Updated node", "nodeType": "BOUNDARY", }, }, &result) require.NoError(t, err) assert.Equal(t, "Updated node", result.UpdateRiskAssessmentNode.RiskAssessmentNode.Name) assert.Equal(t, "BOUNDARY", result.UpdateRiskAssessmentNode.RiskAssessmentNode.NodeType) } func TestRiskAssessmentProcess_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) src := factory.CreateRiskAssessmentNode(owner, scopeID, factory.Attrs{"nodeType": "ENTITY"}) dst := factory.CreateRiskAssessmentNode(owner, scopeID, factory.Attrs{"nodeType": "ASSET"}) processID := factory.CreateRiskAssessmentProcess(owner, scopeID, src, dst) var result struct { UpdateRiskAssessmentProcess struct { RiskAssessmentProcess struct { ID string `json:"id"` Name string `json:"name"` } `json:"riskAssessmentProcess"` } `json:"updateRiskAssessmentProcess"` } err := owner.Execute(` mutation($input: UpdateRiskAssessmentProcessInput!) { updateRiskAssessmentProcess(input: $input) { riskAssessmentProcess { id name } } } `, map[string]any{ "input": map[string]any{ "id": processID, "name": "Updated process", }, }, &result) require.NoError(t, err) assert.Equal(t, "Updated process", result.UpdateRiskAssessmentProcess.RiskAssessmentProcess.Name) } func TestRiskAssessmentThreat_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) src := factory.CreateRiskAssessmentNode(owner, scopeID) dst := factory.CreateRiskAssessmentNode(owner, scopeID) processID := factory.CreateRiskAssessmentProcess(owner, scopeID, src, dst) threatID := factory.CreateRiskAssessmentThreat(owner, scopeID, processID, factory.Attrs{"name": "Original", "category": "Confidentiality"}) var result struct { UpdateRiskAssessmentThreat struct { RiskAssessmentThreat struct { ID string `json:"id"` Name string `json:"name"` Category string `json:"category"` } `json:"riskAssessmentThreat"` } `json:"updateRiskAssessmentThreat"` } err := owner.Execute(` mutation($input: UpdateRiskAssessmentThreatInput!) { updateRiskAssessmentThreat(input: $input) { riskAssessmentThreat { id name category } } } `, map[string]any{ "input": map[string]any{ "id": threatID, "name": "Updated threat", "category": "Integrity", }, }, &result) require.NoError(t, err) assert.Equal(t, "Updated threat", result.UpdateRiskAssessmentThreat.RiskAssessmentThreat.Name) assert.Equal(t, "Integrity", result.UpdateRiskAssessmentThreat.RiskAssessmentThreat.Category) } func TestRiskAssessmentScenario_Update(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) scenarioID := factory.CreateRiskAssessmentScenario(owner, scopeID, factory.Attrs{"name": "Original"}) var result struct { UpdateRiskAssessmentScenario struct { RiskAssessmentScenario struct { ID string `json:"id"` Name string `json:"name"` Description *string `json:"description"` } `json:"riskAssessmentScenario"` } `json:"updateRiskAssessmentScenario"` } err := owner.Execute(` mutation($input: UpdateRiskAssessmentScenarioInput!) { updateRiskAssessmentScenario(input: $input) { riskAssessmentScenario { id name description } } } `, map[string]any{ "input": map[string]any{ "id": scenarioID, "name": "Updated scenario", "description": "Scenario desc", }, }, &result) require.NoError(t, err) assert.Equal(t, "Updated scenario", result.UpdateRiskAssessmentScenario.RiskAssessmentScenario.Name) require.NotNil(t, result.UpdateRiskAssessmentScenario.RiskAssessmentScenario.Description) assert.Equal(t, "Scenario desc", *result.UpdateRiskAssessmentScenario.RiskAssessmentScenario.Description) } func TestRiskAssessmentScenario_LinkUnlinkThreat(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) src := factory.CreateRiskAssessmentNode(owner, scopeID) dst := factory.CreateRiskAssessmentNode(owner, scopeID) processID := factory.CreateRiskAssessmentProcess(owner, scopeID, src, dst) threatID := factory.CreateRiskAssessmentThreat(owner, scopeID, processID) scenarioID := factory.CreateRiskAssessmentScenario(owner, scopeID) factory.LinkRiskAssessmentScenarioThreat(owner, scenarioID, threatID) var result struct { Node struct { Threats struct { TotalCount int `json:"totalCount"` } `json:"threats"` } `json:"node"` } err := owner.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessmentScenario { threats(first: 10) { totalCount } } } } `, map[string]any{"id": scenarioID}, &result) require.NoError(t, err) assert.Equal(t, 1, result.Node.Threats.TotalCount) _, err = owner.Do(` mutation($input: UnlinkRiskAssessmentScenarioThreatInput!) { unlinkRiskAssessmentScenarioThreat(input: $input) { riskAssessmentScenario { id } } } `, map[string]any{ "input": map[string]any{ "riskAssessmentScenarioId": scenarioID, "threatId": threatID, }, }) require.NoError(t, err) err = owner.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessmentScenario { threats(first: 10) { totalCount } } } } `, map[string]any{"id": scenarioID}, &result) require.NoError(t, err) assert.Equal(t, 0, result.Node.Threats.TotalCount) } func TestRiskAssessmentScenario_LinkUnlinkRisk(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner) scopeID := factory.CreateRiskAssessmentScope(owner, raID) riskID := factory.CreateRisk(owner) scenarioID := factory.CreateRiskAssessmentScenario(owner, scopeID) factory.LinkRiskAssessmentScenarioRisk(owner, scenarioID, riskID) var result struct { Node struct { Risks struct { TotalCount int `json:"totalCount"` } `json:"risks"` } `json:"node"` } err := owner.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessmentScenario { risks(first: 10) { totalCount } } } } `, map[string]any{"id": scenarioID}, &result) require.NoError(t, err) assert.Equal(t, 1, result.Node.Risks.TotalCount) _, err = owner.Do(` mutation($input: UnlinkRiskAssessmentScenarioRiskInput!) { unlinkRiskAssessmentScenarioRisk(input: $input) { riskAssessmentScenario { id } } } `, map[string]any{ "input": map[string]any{ "riskAssessmentScenarioId": scenarioID, "riskId": riskID, }, }) require.NoError(t, err) err = owner.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessmentScenario { risks(first: 10) { totalCount } } } } `, map[string]any{"id": scenarioID}, &result) require.NoError(t, err) assert.Equal(t, 0, result.Node.Risks.TotalCount) } func TestRiskAssessment_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) _, err := viewer.Do(` mutation($input: CreateRiskAssessmentInput!) { createRiskAssessment(input: $input) { riskAssessmentEdge { node { id } } } } `, map[string]any{ "input": map[string]any{ "organizationId": viewer.GetOrganizationID().String(), "name": "test", }, }) testutil.RequireForbiddenError(t, err, "viewer cannot create risk assessment") }) 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, 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 RiskAssessment { id name } } } `, map[string]any{"id": raID}, &result) require.NoError(t, err) assert.Equal(t, "Visible", result.Node.Name) }) } func TestRiskAssessment_TenantIsolation(t *testing.T) { t.Parallel() owner1 := testutil.NewClient(t, testutil.RoleOwner) owner2 := testutil.NewClient(t, testutil.RoleOwner) raID := factory.CreateRiskAssessment(owner1) var result struct { Node *struct { ID string `json:"id"` } `json:"node"` } err := owner2.Execute(` query($id: ID!) { node(id: $id) { ... on RiskAssessment { id } } } `, map[string]any{"id": raID}, &result) testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "RiskAssessment") }