Skip to content

Commit d5f6b5c

Browse files
committed
fix: rabbit feedback for disconnected paths
1 parent a965703 commit d5f6b5c

2 files changed

Lines changed: 84 additions & 19 deletions

File tree

packages/go/analysis/azure/azure_integration_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,3 +1044,79 @@ func TestFetchEntityDescendentPaths_NoTerminalsReturnsEmpty(t *testing.T) {
10441044
})
10451045
require.NoError(t, err)
10461046
}
1047+
1048+
// TestFetchEntityDescendentPaths_SharedIntermediateNode verifies that two terminal nodes sharing
1049+
// an intermediate container node both produce complete paths reaching root. Each traversal
1050+
// independently reaches root through the shared intermediate without being truncated.
1051+
func TestFetchEntityDescendentPaths_SharedIntermediateNode(t *testing.T) {
1052+
t.Parallel()
1053+
1054+
suite := setupIntegrationTestSuite(t)
1055+
defer teardownIntegrationTestSuite(t, &suite)
1056+
1057+
var (
1058+
tenantID = integration.RandomObjectID(t)
1059+
tenantNode = NewAzureTenant(t, &suite, tenantID)
1060+
subscriptionNode = NewNode(t, &suite, graph.AsProperties(graph.PropertyMap{
1061+
common.Name: "SharedSubscription",
1062+
common.ObjectID: integration.RandomObjectID(t),
1063+
graphAzure.TenantID: tenantID,
1064+
}), graphAzure.Entity, graphAzure.Subscription)
1065+
app1 = NewAzureApplication(t, &suite, "App1", integration.RandomObjectID(t), tenantID)
1066+
app2 = NewAzureApplication(t, &suite, "App2", integration.RandomObjectID(t), tenantID)
1067+
)
1068+
1069+
// tenant --Contains--> subscription --Contains--> app1
1070+
// --Contains--> app2
1071+
NewRelationship(t, &suite, tenantNode, subscriptionNode, graphAzure.Contains)
1072+
NewRelationship(t, &suite, subscriptionNode, app1, graphAzure.Contains)
1073+
NewRelationship(t, &suite, subscriptionNode, app2, graphAzure.Contains)
1074+
1075+
err := suite.GraphDB.ReadTransaction(suite.Context, func(tx graph.Transaction) error {
1076+
paths, err := azure.FetchEntityDescendentPaths(tx, tenantNode, graphAzure.App)
1077+
require.NoError(t, err)
1078+
1079+
nodeIDs := paths.AllNodes().IDs()
1080+
require.Contains(t, nodeIDs, tenantNode.ID)
1081+
require.Contains(t, nodeIDs, subscriptionNode.ID)
1082+
require.Contains(t, nodeIDs, app1.ID)
1083+
require.Contains(t, nodeIDs, app2.ID)
1084+
1085+
return nil
1086+
})
1087+
require.NoError(t, err)
1088+
}
1089+
1090+
// TestFetchEntityDescendentPaths_TerminalNotConnectedToRoot verifies that a terminal node
1091+
// which exists in the same tenant but has no azure.Contains path leading back to root
1092+
// is excluded from the returned path set.
1093+
func TestFetchEntityDescendentPaths_TerminalNotConnectedToRoot(t *testing.T) {
1094+
t.Parallel()
1095+
1096+
suite := setupIntegrationTestSuite(t)
1097+
defer teardownIntegrationTestSuite(t, &suite)
1098+
1099+
var (
1100+
tenantID = integration.RandomObjectID(t)
1101+
tenantNode = NewAzureTenant(t, &suite, tenantID)
1102+
connectedApp = NewAzureApplication(t, &suite, "ConnectedApp", integration.RandomObjectID(t), tenantID)
1103+
disconnectedApp = NewAzureApplication(t, &suite, "DisconnectedApp", integration.RandomObjectID(t), tenantID)
1104+
)
1105+
1106+
// Only connectedApp has a Contains edge to root; disconnectedApp shares the tenantID
1107+
// but has no path leading back to tenantNode.
1108+
NewRelationship(t, &suite, tenantNode, connectedApp, graphAzure.Contains)
1109+
1110+
err := suite.GraphDB.ReadTransaction(suite.Context, func(tx graph.Transaction) error {
1111+
paths, err := azure.FetchEntityDescendentPaths(tx, tenantNode, graphAzure.App)
1112+
require.NoError(t, err)
1113+
1114+
nodeIDs := paths.AllNodes().IDs()
1115+
require.Contains(t, nodeIDs, tenantNode.ID)
1116+
require.Contains(t, nodeIDs, connectedApp.ID)
1117+
require.NotContains(t, nodeIDs, disconnectedApp.ID)
1118+
1119+
return nil
1120+
})
1121+
require.NoError(t, err)
1122+
}

packages/go/analysis/azure/queries.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import (
2828
"github.com/specterops/bloodhound/packages/go/graphschema/ad"
2929
"github.com/specterops/bloodhound/packages/go/graphschema/azure"
3030
"github.com/specterops/bloodhound/packages/go/graphschema/common"
31-
"github.com/specterops/dawgs/cardinality"
3231
"github.com/specterops/dawgs/graph"
3332
"github.com/specterops/dawgs/ops"
3433
"github.com/specterops/dawgs/query"
@@ -872,46 +871,36 @@ func FetchDescendentKindByTenantID(tx graph.Transaction, root *graph.Node, desce
872871

873872
// FetchEntityDescendentPaths fetches paths from each terminal node of descendentKind (within the
874873
// same Azure tenant as root) back up to root via azure.Contains relationships. Each traversal
875-
// halts upon reaching root or encountering a node already claimed by a prior traversal, preventing
876-
// duplicate path segments across traversals.
874+
// halts upon reaching root
877875
func FetchEntityDescendentPaths(tx graph.Transaction, root *graph.Node, descendentKind ...graph.Kind) (graph.PathSet, error) {
878-
var (
879-
visitedBitmap = cardinality.NewBitmap64()
880-
pathSet = graph.NewPathSet()
881-
)
882-
883-
// Pre-populate the visited bitmap with root so that any traversal halts when it reaches it
884-
visitedBitmap.Add(root.ID.Uint64())
876+
pathSet := graph.NewPathSet()
885877

886878
terminalNodes, err := FetchDescendentKindByTenantID(tx, root, descendentKind...)
887879
if err != nil {
888880
return nil, err
889881
}
890882

891883
for _, terminalNode := range terminalNodes {
892-
// Skip this terminal if it was already reached and claimed by a prior traversal
893-
if visitedBitmap.Contains(terminalNode.ID.Uint64()) {
894-
continue
895-
}
896-
884+
reachedRoot := false
897885
if paths, err := ops.TraversePaths(tx, ops.TraversalPlan{
898886
Root: terminalNode,
899887
Direction: graph.DirectionInbound,
900888
BranchQuery: func() graph.Criteria {
901889
return query.Kind(query.Relationship(), azure.Contains)
902890
},
903891
ExpansionFilter: func(segment *graph.PathSegment) bool {
904-
nodeID := segment.Node.ID.Uint64()
905-
if visitedBitmap.Contains(nodeID) {
892+
if segment.Node.ID == root.ID {
893+
reachedRoot = true
906894
return false
907895
}
908-
visitedBitmap.Add(nodeID)
909896
return true
910897
},
911898
}); err != nil {
912899
return nil, err
913900
} else {
914-
pathSet.AddPathSet(paths)
901+
if reachedRoot {
902+
pathSet.AddPathSet(paths)
903+
}
915904
}
916905
}
917906

0 commit comments

Comments
 (0)