Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f732fab
add kinds method
brandonshearin Jan 23, 2026
b3b3d11
new db method
brandonshearin Jan 23, 2026
cd9ea4f
working on og-tool
brandonshearin Jan 23, 2026
61c3098
mocks
brandonshearin Jan 27, 2026
9537c25
Merge branch 'main' into BED-6903
brandonshearin Jan 27, 2026
6c04cb8
Merge branch 'main' into BED-6903
brandonshearin Jan 28, 2026
58fcf09
generate metatrees double loop extensions -> environments
brandonshearin Jan 29, 2026
1e14852
uppercase environmetn_id on nodes
brandonshearin Feb 2, 2026
299c714
Merge branch 'main' into BED-6903
brandonshearin Feb 3, 2026
eddcd85
just generate
brandonshearin Feb 3, 2026
309111e
Tenant is incorrect
brandonshearin Feb 3, 2026
912af8b
just prepare for codereview
brandonshearin Feb 3, 2026
2f0f74f
consolidate getenv methods
brandonshearin Feb 3, 2026
fcf0fd0
single helper
brandonshearin Feb 3, 2026
d83a5f9
remove raise exceptions -> genscript upsert kind
brandonshearin Feb 6, 2026
974e483
better type for sourcekind methods
brandonshearin Feb 6, 2026
de91119
drop compound constraint on schema_environments
brandonshearin Feb 6, 2026
60e539e
fix logic for getenvbykindid
brandonshearin Feb 6, 2026
717fe7d
embed tierID
brandonshearin Feb 9, 2026
589c140
merge w main, this was painful
brandonshearin Feb 9, 2026
4b91907
add back in GetEnvironmentById
brandonshearin Feb 9, 2026
fc2af66
sourcekind cleanup
brandonshearin Feb 9, 2026
d9b15a4
getsourcekindbyids variadic
brandonshearin Feb 10, 2026
4785d45
getkindsbyids
brandonshearin Feb 10, 2026
6b09cc0
sourcekind integration test
brandonshearin Feb 10, 2026
d2cc881
purpose-built func for getting rels by extID
brandonshearin Feb 10, 2026
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
2 changes: 1 addition & 1 deletion cmd/api/src/api/v2/assetgrouptags.go
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ func buildAssetGroupMembersByTagGraphDbFilters(ctx context.Context, db database.
return filters, err
} else {
for _, kind := range sourceKinds {
sourceKindsMap[kind.Name.String()] = true
sourceKindsMap[kind.Name] = true
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/api/src/api/v2/database_wipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func (s Resources) BuildDeleteRequest(ctx context.Context, userID string, payloa
found := false
for _, sk := range sourceKinds {
if sk.ID == id {
requestedKinds = append(requestedKinds, sk.Name)
requestedKinds = append(requestedKinds, sk.ToKind())
found = true
break
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/api/src/api/v2/kinds.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (s Resources) ListSourceKinds(response http.ResponseWriter, request *http.R
} else {
// inject 0, Sourceless into the payload. We don't track this as an official kind
// but it will facilitate delete requests for data that isn't associated with a kind.
kinds = append(kinds, database.SourceKind{ID: 0, Name: graph.StringKind("Sourceless")})
kinds = append(kinds, database.SourceKind{ID: 0, Name: "Sourceless"})
api.WriteBasicResponse(request.Context(), ListSourceKindsResponse{Kinds: kinds}, http.StatusOK, response)
}
}
2 changes: 1 addition & 1 deletion cmd/api/src/daemons/datapipe/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func PurgeGraphData(
func extractKindNames(sourceKinds []database.SourceKind) graph.Kinds {
var kinds graph.Kinds
for _, k := range sourceKinds {
kinds = append(kinds, k.Name)
kinds = append(kinds, k.ToKind())
}
return kinds
}
Expand Down
170 changes: 130 additions & 40 deletions cmd/api/src/database/graphschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,23 @@ type OpenGraphSchema interface {
CreateGraphSchemaRelationshipKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaRelationshipKind, error)
GetGraphSchemaRelationshipKinds(ctx context.Context, filters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaRelationshipKinds, int, error)
GetGraphSchemaRelationshipKindById(ctx context.Context, schemaRelationshipKindId int32) (model.GraphSchemaRelationshipKind, error)
GetTraversableRelationshipKindsByExtensionID(ctx context.Context, extensionID int32) (model.GraphSchemaRelationshipKinds, error)
UpdateGraphSchemaRelationshipKind(ctx context.Context, schemaRelationshipKind model.GraphSchemaRelationshipKind) (model.GraphSchemaRelationshipKind, error)
DeleteGraphSchemaRelationshipKind(ctx context.Context, schemaRelationshipKindId int32) error

GetGraphSchemaRelationshipKindsWithSchemaName(ctx context.Context, filters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaRelationshipKindsWithNamedSchema, int, error)

CreateEnvironment(ctx context.Context, extensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error)
GetEnvironmentByKinds(ctx context.Context, environmentKindId, sourceKindId int32) (model.SchemaEnvironment, error)
GetEnvironmentByEnvironmentKindId(ctx context.Context, environmentKindId int32) (model.SchemaEnvironment, error)
GetEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error)
GetEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error)
GetEnvironmentsFiltered(ctx context.Context, filters model.Filters) ([]model.SchemaEnvironment, error)
DeleteEnvironment(ctx context.Context, environmentId int32) error

CreateSchemaRelationshipFinding(ctx context.Context, extensionId int32, relationshipKindId int32, environmentId int32, name string, displayName string) (model.SchemaRelationshipFinding, error)
GetSchemaRelationshipFindingById(ctx context.Context, findingId int32) (model.SchemaRelationshipFinding, error)
GetSchemaRelationshipFindingByName(ctx context.Context, name string) (model.SchemaRelationshipFinding, error)
GetSchemaRelationshipFindingsByEnvironmentId(ctx context.Context, environmentId int32) ([]model.SchemaRelationshipFinding, error)
DeleteSchemaRelationshipFinding(ctx context.Context, findingId int32) error

CreateRemediation(ctx context.Context, findingId int32, shortDescription string, longDescription string, shortRemediation string, longRemediation string) (model.Remediation, error)
Expand Down Expand Up @@ -494,6 +497,24 @@ func (s *BloodhoundDB) GetGraphSchemaRelationshipKinds(ctx context.Context, rela
}
}

// GetTraversableRelationshipKindsByExtensionID returns all traversable relationship kinds for a given schema extension.
// This is a purpose-built query for the analysis pipeline that needs traversable edges for graph traversal.
func (s *BloodhoundDB) GetTraversableRelationshipKindsByExtensionID(ctx context.Context, extensionID int32) (model.GraphSchemaRelationshipKinds, error) {
const query = `
SELECT rk.id, k.name, rk.schema_extension_id, rk.description, rk.is_traversable,
rk.created_at, rk.updated_at, rk.deleted_at
FROM schema_relationship_kinds rk
JOIN kind k ON rk.kind_id = k.id
WHERE rk.schema_extension_id = $1 AND rk.is_traversable = true
`

var kinds model.GraphSchemaRelationshipKinds
if result := s.db.WithContext(ctx).Raw(query, extensionID).Scan(&kinds); result.Error != nil {
return nil, CheckError(result)
}
return kinds, nil
}

func (s *BloodhoundDB) GetGraphSchemaRelationshipKindsWithSchemaName(ctx context.Context, relationshipKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaRelationshipKindsWithNamedSchema, int, error) {
var (
schemaRelationshipKinds = model.GraphSchemaRelationshipKindsWithNamedSchema{}
Expand Down Expand Up @@ -601,11 +622,24 @@ func (s *BloodhoundDB) CreateEnvironment(ctx context.Context, extensionId int32,
return schemaEnvironment, nil
}

// GetEnvironments - retrieves list of schema environments.
func (s *BloodhoundDB) GetEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) {
// GetEnvironmentsFiltered - retrieves schema environments filtered by the given criteria.
// This is the core implementation that all other GetEnvironment* methods delegate to.
// Common use case: filter by schema_extension_id to get all environments for a specific extension.
// Example: filters := model.Filters{"se.schema_extension_id": []model.Filter{{Operator: model.Equals, Value: "1"}}}
func (s *BloodhoundDB) GetEnvironmentsFiltered(ctx context.Context, filters model.Filters) ([]model.SchemaEnvironment, error) {
var result []model.SchemaEnvironment

query := `
sqlFilter, err := buildSQLFilter(filters)
if err != nil {
return nil, err
}

whereClause := ""
if sqlFilter.sqlString != "" {
whereClause = fmt.Sprintf("WHERE %s", sqlFilter.sqlString)
}

query := fmt.Sprintf(`
SELECT
se.id,
se.schema_extension_id,
Expand All @@ -619,9 +653,10 @@ func (s *BloodhoundDB) GetEnvironments(ctx context.Context) ([]model.SchemaEnvir
FROM schema_environments se
INNER JOIN kind k ON se.environment_kind_id = k.id
INNER JOIN schema_extensions ext ON se.schema_extension_id = ext.id
ORDER BY se.id`
%s
ORDER BY se.id`, whereClause)

if err := CheckError(s.db.WithContext(ctx).Raw(query).Scan(&result)); err != nil {
if err := CheckError(s.db.WithContext(ctx).Raw(query, sqlFilter.params...).Scan(&result)); err != nil {
return nil, err
}

Expand All @@ -632,6 +667,12 @@ func (s *BloodhoundDB) GetEnvironments(ctx context.Context) ([]model.SchemaEnvir
return result, nil
}

// GetEnvironments - retrieves all schema environments.
func (s *BloodhoundDB) GetEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) {
return s.GetEnvironmentsFiltered(ctx, model.Filters{})

}

// GetEnvironmentsByExtensionId - retrieves a slice of model.SchemaEnvironment by extension id.
func (s *BloodhoundDB) GetEnvironmentsByExtensionId(ctx context.Context, extensionId int32) ([]model.SchemaEnvironment, error) {
var (
Expand All @@ -652,22 +693,6 @@ func (s *BloodhoundDB) GetEnvironmentsByExtensionId(ctx context.Context, extensi

}

// GetEnvironmentByKinds - retrieves an environment by its environment kind and source kind.
func (s *BloodhoundDB) GetEnvironmentByKinds(ctx context.Context, environmentKindId, sourceKindId int32) (model.SchemaEnvironment, error) {
var env model.SchemaEnvironment

if result := s.db.WithContext(ctx).Raw(
"SELECT * FROM schema_environments WHERE environment_kind_id = ? AND source_kind_id = ? AND deleted_at IS NULL",
environmentKindId, sourceKindId,
).Scan(&env); result.Error != nil {
return model.SchemaEnvironment{}, CheckError(result)
} else if result.RowsAffected == 0 {
return model.SchemaEnvironment{}, ErrNotFound
}

return env, nil
}

// GetEnvironmentById - retrieves a schema environment by id.
func (s *BloodhoundDB) GetEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error) {
var schemaEnvironment model.SchemaEnvironment
Expand All @@ -685,6 +710,23 @@ func (s *BloodhoundDB) GetEnvironmentById(ctx context.Context, environmentId int
return schemaEnvironment, nil
}

// GetEnvironmentByEnvironmentKindId - retrieves a schema environment by environment_kind_id.
func (s *BloodhoundDB) GetEnvironmentByEnvironmentKindId(ctx context.Context, environmentKindId int32) (model.SchemaEnvironment, error) {
filters := model.Filters{
"se.environment_kind_id": []model.Filter{{Operator: model.Equals, Value: fmt.Sprintf("%d", environmentKindId)}},
}

envs, err := s.GetEnvironmentsFiltered(ctx, filters)
if err != nil {
return model.SchemaEnvironment{}, err
}
if len(envs) == 0 {
return model.SchemaEnvironment{}, ErrNotFound
}

return envs[0], nil
}

// DeleteEnvironment - deletes a schema environment by id.
func (s *BloodhoundDB) DeleteEnvironment(ctx context.Context, environmentId int32) error {
var schemaEnvironment model.SchemaEnvironment
Expand Down Expand Up @@ -716,38 +758,86 @@ func (s *BloodhoundDB) CreateSchemaRelationshipFinding(ctx context.Context, exte
return finding, nil
}

// getSchemaRelationshipFindingsFiltered - retrieves schema relationship findings filtered by the given criteria.
// This is the core implementation that all other GetSchemaRelationshipFinding* methods delegate to.
func (s *BloodhoundDB) getSchemaRelationshipFindingsFiltered(ctx context.Context, filters model.Filters) ([]model.SchemaRelationshipFinding, error) {
var result []model.SchemaRelationshipFinding

sqlFilter, err := buildSQLFilter(filters)
if err != nil {
return nil, err
}

whereClause := ""
if sqlFilter.sqlString != "" {
whereClause = fmt.Sprintf("WHERE %s", sqlFilter.sqlString)
}

query := fmt.Sprintf(`
SELECT
srf.id,
srf.schema_extension_id,
srf.relationship_kind_id,
srf.environment_id,
srf.name,
srf.display_name,
srf.created_at
FROM schema_relationship_findings srf
%s
ORDER BY srf.id`, whereClause)

if err := CheckError(s.db.WithContext(ctx).Raw(query, sqlFilter.params...).Scan(&result)); err != nil {
return nil, err
}

if result == nil {
result = []model.SchemaRelationshipFinding{}
}

return result, nil
}

// GetSchemaRelationshipFindingById - retrieves a schema relationship finding by id.
func (s *BloodhoundDB) GetSchemaRelationshipFindingById(ctx context.Context, findingId int32) (model.SchemaRelationshipFinding, error) {
var finding model.SchemaRelationshipFinding
filters := model.Filters{
"srf.id": []model.Filter{{Operator: model.Equals, Value: fmt.Sprintf("%d", findingId)}},
}

if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(`
SELECT id, schema_extension_id, relationship_kind_id, environment_id, name, display_name, created_at
FROM %s WHERE id = ?`,
finding.TableName()),
findingId).Scan(&finding); result.Error != nil {
return model.SchemaRelationshipFinding{}, CheckError(result)
} else if result.RowsAffected == 0 {
findings, err := s.getSchemaRelationshipFindingsFiltered(ctx, filters)
if err != nil {
return model.SchemaRelationshipFinding{}, err
}
if len(findings) == 0 {
return model.SchemaRelationshipFinding{}, ErrNotFound
}

return finding, nil
return findings[0], nil
}

// GetSchemaRelationshipFindingByName - retrieves a schema relationship finding by finding name.
func (s *BloodhoundDB) GetSchemaRelationshipFindingByName(ctx context.Context, name string) (model.SchemaRelationshipFinding, error) {
var finding model.SchemaRelationshipFinding
filters := model.Filters{
"srf.name": []model.Filter{{Operator: model.Equals, Value: name}},
}

if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(`
SELECT id, schema_extension_id, relationship_kind_id, environment_id, name, display_name, created_at
FROM %s WHERE name = ?`,
finding.TableName()),
name).Scan(&finding); result.Error != nil {
return model.SchemaRelationshipFinding{}, CheckError(result)
} else if result.RowsAffected == 0 {
findings, err := s.getSchemaRelationshipFindingsFiltered(ctx, filters)
if err != nil {
return model.SchemaRelationshipFinding{}, err
}
if len(findings) == 0 {
return model.SchemaRelationshipFinding{}, ErrNotFound
}

return finding, nil
return findings[0], nil
}

// GetSchemaRelationshipFindingsByEnvironmentId - retrieves all schema relationship findings for a given environment.
func (s *BloodhoundDB) GetSchemaRelationshipFindingsByEnvironmentId(ctx context.Context, environmentId int32) ([]model.SchemaRelationshipFinding, error) {
filters := model.Filters{
"srf.environment_id": []model.Filter{{Operator: model.Equals, Value: fmt.Sprintf("%d", environmentId)}},
}

return s.getSchemaRelationshipFindingsFiltered(ctx, filters)
}

// DeleteSchemaRelationshipFinding - deletes a schema relationship finding by id.
Expand Down
14 changes: 7 additions & 7 deletions cmd/api/src/database/graphschema_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4023,7 +4023,7 @@ func TestDatabase_Environments_CRUD(t *testing.T) {
assert.NoError(t, err, "unexpected error occurred when creating environment")

// Validate created environment is as expected
retrievedEnvironment, err := testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, newEnvironment.ID)
retrievedEnvironment, err := testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, newEnvironment.EnvironmentKindId)
assert.NoError(t, err, "unexpected error occurred when retrieving environment by id")

assertContainsEnvironment(t, retrievedEnvironment, environment)
Expand Down Expand Up @@ -4080,7 +4080,7 @@ func TestDatabase_Environments_CRUD(t *testing.T) {
newEnvironment, err := testSuite.BHDatabase.CreateEnvironment(testSuite.Context, environment.SchemaExtensionId, environment.EnvironmentKindId, environment.SourceKindId)
require.NoError(t, err, "unexpected error occurred when creating environment")

retrievedEnvironment, err := testSuite.BHDatabase.GetEnvironmentByKinds(testSuite.Context, newEnvironment.EnvironmentKindId, newEnvironment.SourceKindId)
retrievedEnvironment, err := testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, newEnvironment.EnvironmentKindId)
assert.NoError(t, err, database.ErrNotFound)

assertContainsEnvironment(t, retrievedEnvironment, environment)
Expand All @@ -4097,7 +4097,7 @@ func TestDatabase_Environments_CRUD(t *testing.T) {
SourceKindId: 257958,
}

_, err := testSuite.BHDatabase.GetEnvironmentByKinds(testSuite.Context, environment.EnvironmentKindId, environment.SourceKindId)
_, err := testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, environment.EnvironmentKindId)
assert.EqualError(t, err, database.ErrNotFound.Error(), "expected entity not found")
},
},
Expand All @@ -4124,7 +4124,7 @@ func TestDatabase_Environments_CRUD(t *testing.T) {
require.NoError(t, err, "unexpected error occurred when creating environment")

// Validate environment
retrievedEnvironment, err := testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, newEnvironment.ID)
retrievedEnvironment, err := testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, newEnvironment.EnvironmentKindId)
assert.NoError(t, err, "failed to get environment by id")

assertContainsEnvironment(t, retrievedEnvironment, environment)
Expand All @@ -4136,7 +4136,7 @@ func TestDatabase_Environments_CRUD(t *testing.T) {
assert: func(t *testing.T, testSuite IntegrationTestSuite) {
t.Helper()

_, err := testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, int32(5000))
_, err := testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, int32(5000))
require.ErrorIs(t, err, database.ErrNotFound)
},
},
Expand Down Expand Up @@ -4265,7 +4265,7 @@ func TestDatabase_Environments_CRUD(t *testing.T) {
assert.NoError(t, err, "unexpected error occurred when deleting environment for extension")

// Validate environment no longer exists
_, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, newEnvironment.ID)
_, err = testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, newEnvironment.EnvironmentKindId)
require.EqualError(t, err, database.ErrNotFound.Error())
},
},
Expand Down Expand Up @@ -5351,7 +5351,7 @@ func TestDeleteSchemaExtension_CascadeDeletesAllDependents(t *testing.T) {
_, err = testSuite.BHDatabase.GetGraphSchemaRelationshipKindById(testSuite.Context, edgeKind.ID)
assert.ErrorIs(t, err, database.ErrNotFound)

_, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, environment.ID)
_, err = testSuite.BHDatabase.GetEnvironmentByEnvironmentKindId(testSuite.Context, environment.EnvironmentKindId)
assert.ErrorIs(t, err, database.ErrNotFound)

_, err = testSuite.BHDatabase.GetSchemaRelationshipFindingById(testSuite.Context, relationshipFinding.ID)
Expand Down
Loading