Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(BED-5436): Constraint violations in LinkWellKnownGroups during ingestion #1190

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 21 additions & 8 deletions packages/go/analysis/ad/ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ func FetchWellKnownTierZeroEntities(ctx context.Context, db graph.Database, doma
func FixWellKnownNodeTypes(ctx context.Context, db graph.Database) error {
defer measure.ContextMeasure(ctx, slog.LevelInfo, "Fix well known node types")()

groupSuffixes := []string{EnterpriseKeyAdminsGroupSIDSuffix,
groupSuffixes := []string{
EnterpriseKeyAdminsGroupSIDSuffix,
KeyAdminsGroupSIDSuffix,
EnterpriseDomainControllersGroupSIDSuffix,
DomainAdminsGroupSIDSuffix,
Expand Down Expand Up @@ -190,7 +191,6 @@ func RunDomainAssociations(ctx context.Context, db graph.Database) error {
// TODO: Reimplement unassociated node pruning if we decide that FOSS needs unassociated node pruning
return nil
})

}

func grabDomainInformation(tx graph.Transaction) (map[string]string, error) {
Expand Down Expand Up @@ -273,15 +273,20 @@ func LinkWellKnownGroups(ctx context.Context, db graph.Database) error {
}
}

func getOrCreateWellKnownGroup(tx graph.Transaction, wellKnownSid string, domainSid, domainName, nodeName string) (*graph.Node, error) {
func getOrCreateWellKnownGroup(
tx graph.Transaction,
wellKnownSid, domainSid, domainName, nodeName string,
) (
*graph.Node,
error,
) {
// Only filter by ObjectID, not by kind
if wellKnownNode, err := tx.Nodes().Filterf(func() graph.Criteria {
return query.And(
query.Equals(query.NodeProperty(common.ObjectID.String()), wellKnownSid),
query.Kind(query.Node(), ad.Group),
)
return query.Equals(query.NodeProperty(common.ObjectID.String()), wellKnownSid)
}).First(); err != nil && !graph.IsErrNotFound(err) {
return nil, err
} else if graph.IsErrNotFound(err) {
// Only create a new node if no node with this ObjectID exists
properties := graph.AsProperties(graph.PropertyMap{
common.Name: nodeName,
common.ObjectID: wellKnownSid,
Expand All @@ -291,6 +296,15 @@ func getOrCreateWellKnownGroup(tx graph.Transaction, wellKnownSid string, domain
})
return tx.CreateNode(properties, ad.Entity, ad.Group)
} else {
// If a node with this ObjectID exists (regardless of its kind), return it
// Optionally, we could add the ad.Group kind if it's missing
if !wellKnownNode.Kinds.ContainsOneOf(ad.Group) {
// Add the ad.Group kind if it's missing
wellKnownNode.AddKinds(ad.Group)
if err := tx.UpdateNode(wellKnownNode); err != nil {
return nil, fmt.Errorf("failed to update node with Group kind: %w", err)
}
}
return wellKnownNode, nil
}
}
Expand Down Expand Up @@ -342,7 +356,6 @@ func CalculateCrossProductNodeSets(tx graph.Transaction, groupExpansions impact.

// Get the IDs of the Auth. Users and Everyone groups
specialGroups, err := FetchAuthUsersAndEveryoneGroups(tx)

if err != nil {
slog.Error(fmt.Sprintf("Could not fetch groups: %s", err.Error()))
}
Expand Down
230 changes: 230 additions & 0 deletions packages/go/analysis/ad/ad_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package ad_test

import (
"context"
"testing"

"github.com/specterops/bloodhound/dawgs/graph"
"github.com/stretchr/testify/require"

adAnalysis "github.com/specterops/bloodhound/analysis/ad"

"github.com/specterops/bloodhound/graphschema/ad"
"github.com/specterops/bloodhound/graphschema/common"
)

func TestLinkWellKnownGroups(t *testing.T) {
// Skip if running short tests
if testing.Short() {
t.Skip("Skipping test in short mode")
}

// Define database drivers to test
dbDrivers := []struct {
name string
driver string
initFn func(t *testing.T, ctx context.Context) (graph.Database, func())
}{
{
name: "Neo4j",
driver: "neo4j",
initFn: initNeo4jDatabase,
},
{
name: "PostgreSQL",
driver: "pg",
initFn: initPostgresDatabase,
},
}

testCases := []linkWellKnownGroupsTestCase{
{
name: "Node Not Found Create New Node",
expectedNode: graph.Node{
Kinds: graph.Kinds{
ad.Entity, ad.Group,
},
Properties: graph.AsProperties(graph.PropertyMap{
common.Name: "some name",
common.ObjectID: "wellknowsid",
ad.DomainSID: "some domain",
ad.DomainFQDN: "fullyqualifieddsomain",
}),
},
setupFunc: func(
t *testing.T,
ctx context.Context,
graphDB graph.Database,
expectedNode *graph.Node,
) *graph.Node {
// NOTE: in order to trigger getOrCreateWellKnownGroup creating a node if the given wellKnownSid does
// not return a node
var createdNode *graph.Node
var err error
err = graphDB.WriteTransaction(ctx, func(tx graph.Transaction) error {
domainProperties := graph.AsProperties(graph.PropertyMap{
common.Name: expectedNode.Properties.Get(common.Name.String()),
common.Collected: true,
})
createdNode, err = tx.CreateNode(domainProperties, ad.Domain)
if err != nil {
return err
}
return tx.Commit()
})
require.NoError(t, err)
require.NotNil(t, createdNode)
return createdNode
},
},
}

// Run tests for each database driver
for _, dbDriver := range dbDrivers {
t.Run(dbDriver.name, func(t *testing.T) {
ctx := context.Background()

// Initialize the database
graphDB, cleanup := dbDriver.initFn(t, ctx)
defer cleanup()

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Clean the database before each test
cleanDatabase(t, ctx, graphDB)

// Set up the test case with a random domain SID and name for each test
// domainSid, domainName := generateDomainInfo()
createdNode := tc.setupFunc(
t,
ctx,
graphDB,
generateCollectedDomain(t),
)
require.NotNil(t, createdNode)

// Verify that the domain node was created successfully
// var domainExists bool
// err := graphDB.ReadTransaction(ctx, func(tx graph.Transaction) error {
// domain, err := tx.Nodes().Filterf(func() graph.Criteria {
// return query.And(
// query.Kind(query.Node(), ad.Domain),
// query.Equals(query.NodeProperty(common.ObjectID.String()), domainSid),
// )
// }).First()
// if err != nil {
// return err
// }
//
// domainExists = (domain != nil)
// return nil
// })
// require.NoError(t, err)
// require.True(t, domainExists, "Domain node was not created successfully")

// Make sure the domain node has the required properties for LinkWellKnownGroups
// err = :graphDB.WriteTransaction(ctx, func(tx graph.Transaction) error {
// domain, err := tx.Nodes().Filterf(func() graph.Criteria {
// return query.And(
// query.Kind(query.Node(), ad.Domain),
// query.Equals(query.NodeProperty(common.ObjectID.String()), domainSid),
// )
// }).First()
// if err != nil {
// return err
// }
//
// // Ensure domain has DomainSID property
// if _, err := domain.Properties.Get(ad.DomainSID.String()).String(); err != nil {
// domain.Properties.Set(ad.DomainSID.String(), domainSid)
// if err := tx.UpdateNode(domain); err != nil {
// return err
// }
// }
// return tx.Commit()
// })
// require.NoError(t, err)

// Verify that the domain node has the DomainSID property set correctly
// err = graphDB.ReadTransaction(ctx, func(tx graph.Transaction) error {
// domain, err := tx.Nodes().Filterf(func() graph.Criteria {
// return query.And(
// query.Kind(query.Node(), ad.Domain),
// query.Equals(query.NodeProperty(common.ObjectID.String()), domainSid),
// )
// }).First()
// if err != nil {
// return err
// }
//
// domainSIDValue, err := domain.Properties.Get(ad.DomainSID.String()).String()
// if err != nil {
// return err
// }
//
// require.Equal(t, domainSid, domainSIDValue, "Domain SID property not set correctly")
// return nil
// })
// require.NoError(t, err)

// Run LinkWellKnownGroups
err := adAnalysis.LinkWellKnownGroups(ctx, graphDB)
require.NoError(t, err)

// Verify the results in a read transaction
err = graphDB.ReadTransaction(ctx, func(tx graph.Transaction) error {
// for sidSuffix, shouldExist := range tc.expectedGroupExists {
// var wellKnownSid string
// if sidSuffix == "-S-1-5-11" || sidSuffix == "-S-1-1-0" {
// // For Authenticated Users and Everyone, the SID is prefixed with the domain name
// wellKnownSid = domainName + sidSuffix
// } else {
// // For other groups, the SID is prefixed with the domain SID
// wellKnownSid = domainSid + sidSuffix
// }
//
// node, err := tx.Nodes().Filterf(func() graph.Criteria {
// return query.Equals(
// query.NodeProperty(common.ObjectID.String()),
// wellKnownSid,
// )
// }).First()
//
// if shouldExist {
// if err != nil {
// return fmt.Errorf(
// "node with SID %s should exist: %w",
// wellKnownSid,
// err,
// )
// }
// if node == nil {
// return fmt.Errorf(
// "node with SID %s should not be nil",
// wellKnownSid,
// )
// }
//
// // Verify the kinds
// // Check that the node has the Group kind
// if !node.Kinds.ContainsOneOf(ad.Group) {
// return fmt.Errorf(
// "node with SID %s should have kind %s",
// wellKnownSid,
// ad.Group,
// )
// }
// } else {
// if err == nil || !graph.IsErrNotFound(err) {
// return fmt.Errorf("node with SID %s should not exist", wellKnownSid)
// }
// }
// }
return nil
})
require.NoError(t, err)
})
}
})
}
}
39 changes: 39 additions & 0 deletions packages/go/analysis/ad/ad_testing_tools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package ad_test

import (
"context"
"testing"

"github.com/specterops/bloodhound/dawgs/graph"
"github.com/specterops/bloodhound/graphschema/ad"
"github.com/specterops/bloodhound/graphschema/common"

"github.com/rs/xid"

faker "github.com/go-faker/faker/v4"
)

func generateCollectedDomain() *graph.Node {
return &graph.Node{
Kinds: graph.Kinds{
ad.Domain,
},
Properties: graph.AsProperties(graph.PropertyMap{
common.Collected: true,
common.Name: faker.DomainName(),
ad.DomainSID: xid.New().String(),
ad.DomainFQDN: faker.DomainName(),
}),
}
}

type linkWellKnownGroupsTestCase struct {
name string
expectedNode graph.Node
setupFunc func(
t *testing.T,
ctx context.Context,
graphDB graph.Database,
expectedNode *graph.Node,
) *graph.Node
}
Loading
Loading