diff --git a/cmd/api/src/api/registration/registration.go b/cmd/api/src/api/registration/registration.go index 179298a0598..20cf7ff25a0 100644 --- a/cmd/api/src/api/registration/registration.go +++ b/cmd/api/src/api/registration/registration.go @@ -64,6 +64,7 @@ func RegisterFossRoutes( authorizer auth.Authorizer, ingestSchema upload.IngestSchema, dogtagsService dogtags.Service, + openGraphSchemaService v2.OpenGraphSchemaService, ) { router.With(func() mux.MiddlewareFunc { return middleware.DefaultRateLimitMiddleware(rdms) @@ -82,6 +83,6 @@ func RegisterFossRoutes( routerInst.PathPrefix("/ui", static.AssetHandler), ) - var resources = v2.NewResources(rdms, graphDB, cfg, apiCache, graphQuery, collectorManifests, authorizer, authenticator, ingestSchema, dogtagsService) + var resources = v2.NewResources(rdms, graphDB, cfg, apiCache, graphQuery, collectorManifests, authorizer, authenticator, ingestSchema, dogtagsService, openGraphSchemaService) NewV2API(resources, routerInst) } diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index 16d524e135e..68ff31ba169 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -367,5 +367,7 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) { routerInst.POST("/api/v2/custom-nodes", resources.CreateCustomNodeKind).RequireAuth(), routerInst.PUT(fmt.Sprintf("/api/v2/custom-nodes/{%s}", v2.CustomNodeKindParameter), resources.UpdateCustomNodeKind).RequireAuth(), routerInst.DELETE(fmt.Sprintf("/api/v2/custom-nodes/{%s}", v2.CustomNodeKindParameter), resources.DeleteCustomNodeKind).RequireAuth(), + + routerInst.PUT("/api/v2/extensions", resources.OpenGraphSchemaIngest), ) } diff --git a/cmd/api/src/api/v2/mocks/graphschemaextensions.go b/cmd/api/src/api/v2/mocks/graphschemaextensions.go new file mode 100644 index 00000000000..21803d8b7b6 --- /dev/null +++ b/cmd/api/src/api/v2/mocks/graphschemaextensions.go @@ -0,0 +1,72 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/specterops/bloodhound/cmd/api/src/api/v2 (interfaces: OpenGraphSchemaService) +// +// Generated by this command: +// +// mockgen -copyright_file ../../../../../LICENSE.header -destination=./mocks/graphschemaextensions.go -package=mocks . OpenGraphSchemaService +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2" + gomock "go.uber.org/mock/gomock" +) + +// MockOpenGraphSchemaService is a mock of OpenGraphSchemaService interface. +type MockOpenGraphSchemaService struct { + ctrl *gomock.Controller + recorder *MockOpenGraphSchemaServiceMockRecorder + isgomock struct{} +} + +// MockOpenGraphSchemaServiceMockRecorder is the mock recorder for MockOpenGraphSchemaService. +type MockOpenGraphSchemaServiceMockRecorder struct { + mock *MockOpenGraphSchemaService +} + +// NewMockOpenGraphSchemaService creates a new mock instance. +func NewMockOpenGraphSchemaService(ctrl *gomock.Controller) *MockOpenGraphSchemaService { + mock := &MockOpenGraphSchemaService{ctrl: ctrl} + mock.recorder = &MockOpenGraphSchemaServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOpenGraphSchemaService) EXPECT() *MockOpenGraphSchemaServiceMockRecorder { + return m.recorder +} + +// UpsertGraphSchemaExtension mocks base method. +func (m *MockOpenGraphSchemaService) UpsertGraphSchemaExtension(ctx context.Context, req v2.GraphSchemaExtension) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertGraphSchemaExtension", ctx, req) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertGraphSchemaExtension indicates an expected call of UpsertGraphSchemaExtension. +func (mr *MockOpenGraphSchemaServiceMockRecorder) UpsertGraphSchemaExtension(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertGraphSchemaExtension", reflect.TypeOf((*MockOpenGraphSchemaService)(nil).UpsertGraphSchemaExtension), ctx, req) +} diff --git a/cmd/api/src/api/v2/model.go b/cmd/api/src/api/v2/model.go index f9a08e13c5c..a91b5122759 100644 --- a/cmd/api/src/api/v2/model.go +++ b/cmd/api/src/api/v2/model.go @@ -116,6 +116,7 @@ type Resources struct { Authenticator api.Authenticator IngestSchema upload.IngestSchema FileService fs.Service + openGraphSchemaService OpenGraphSchemaService DogTags dogtags.Service } @@ -130,6 +131,7 @@ func NewResources( authenticator api.Authenticator, ingestSchema upload.IngestSchema, dogtagsService dogtags.Service, + openGraphSchemaService OpenGraphSchemaService, ) Resources { return Resources{ Decoder: schema.NewDecoder(), @@ -145,5 +147,6 @@ func NewResources( IngestSchema: ingestSchema, FileService: &fs.Client{}, DogTags: dogtagsService, + openGraphSchemaService: openGraphSchemaService, } } diff --git a/cmd/api/src/api/v2/opengraphschema.go b/cmd/api/src/api/v2/opengraphschema.go new file mode 100644 index 00000000000..24cf755b49c --- /dev/null +++ b/cmd/api/src/api/v2/opengraphschema.go @@ -0,0 +1,60 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package v2 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/specterops/bloodhound/cmd/api/src/api" +) + +//go:generate go run go.uber.org/mock/mockgen -copyright_file ../../../../../LICENSE.header -destination=./mocks/graphschemaextensions.go -package=mocks . OpenGraphSchemaService +type OpenGraphSchemaService interface { + UpsertGraphSchemaExtension(ctx context.Context, req GraphSchemaExtension) error +} + +type GraphSchemaExtension struct { + Environments []Environment `json:"environments"` +} + +type Environment struct { + EnvironmentKind string `json:"environmentKind"` + SourceKind string `json:"sourceKind"` + PrincipalKinds []string `json:"principalKinds"` +} + +// TODO: Implement this - skeleton endpoint to simply test the handler. +func (s Resources) OpenGraphSchemaIngest(response http.ResponseWriter, request *http.Request) { + var ( + ctx = request.Context() + ) + + var req GraphSchemaExtension + if err := json.NewDecoder(request.Body).Decode(&req); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponsePayloadUnmarshalError, request), response) + return + } + + if err := s.openGraphSchemaService.UpsertGraphSchemaExtension(ctx, req); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("error upserting graph schema extension: %v", err), request), response) + return + } + + response.WriteHeader(http.StatusCreated) +} diff --git a/cmd/api/src/database/db.go b/cmd/api/src/database/db.go index 7f776d595f8..acee7c7914e 100644 --- a/cmd/api/src/database/db.go +++ b/cmd/api/src/database/db.go @@ -189,6 +189,9 @@ type Database interface { // OpenGraph Schema OpenGraphSchema + + // Kind + Kind } type BloodhoundDB struct { diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 03d5e530143..3588f3f79bb 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -52,10 +52,11 @@ type OpenGraphSchema interface { GetGraphSchemaEdgeKindsWithSchemaName(ctx context.Context, edgeKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaEdgeKindsWithNamedSchema, int, error) - CreateSchemaEnvironment(ctx context.Context, extensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error) - GetSchemaEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error) - GetSchemaEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) - DeleteSchemaEnvironment(ctx context.Context, environmentId int32) error + CreateEnvironment(ctx context.Context, extensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error) + GetEnvironmentByKinds(ctx context.Context, environmentKindId, sourceKindId int32) (model.SchemaEnvironment, error) + GetEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error) + GetEnvironments(ctx context.Context) ([]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) @@ -65,9 +66,10 @@ type OpenGraphSchema interface { GetRemediationByFindingId(ctx context.Context, findingId int32) (model.Remediation, error) UpdateRemediation(ctx context.Context, findingId int32, shortDescription string, longDescription string, shortRemediation string, longRemediation string) (model.Remediation, error) DeleteRemediation(ctx context.Context, findingId int32) error - CreateSchemaEnvironmentPrincipalKind(ctx context.Context, environmentId int32, principalKind int32) (model.SchemaEnvironmentPrincipalKind, error) - GetSchemaEnvironmentPrincipalKindsByEnvironmentId(ctx context.Context, environmentId int32) (model.SchemaEnvironmentPrincipalKinds, error) - DeleteSchemaEnvironmentPrincipalKind(ctx context.Context, environmentId int32, principalKind int32) error + + CreatePrincipalKind(ctx context.Context, environmentId int32, principalKind int32) (model.SchemaEnvironmentPrincipalKind, error) + GetPrincipalKindsByEnvironmentId(ctx context.Context, environmentId int32) (model.SchemaEnvironmentPrincipalKinds, error) + DeletePrincipalKind(ctx context.Context, environmentId int32, principalKind int32) error } const ( @@ -239,10 +241,10 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters mode if filterAndPagination, err := parseFiltersAndPagination(filters, sort, skip, limit); err != nil { return schemaNodeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT nk.id, k.name, nk.schema_extension_id, nk.display_name, nk.description, + sqlStr := fmt.Sprintf(`SELECT nk.id, k.name, nk.schema_extension_id, nk.display_name, nk.description, nk.is_display_kind, nk.icon, nk.icon_color, nk.created_at, nk.updated_at, nk.deleted_at FROM %s nk - JOIN %s k ON nk.kind_id = k.id + JOIN %s k ON nk.kind_id = k.id %s %s %s`, model.GraphSchemaNodeKind{}.TableName(), kindTable, filterAndPagination.WhereClause, filterAndPagination.OrderSql, filterAndPagination.SkipLimit) if result := s.db.WithContext(ctx).Raw(sqlStr, filterAndPagination.Filter.params...).Scan(&schemaNodeKinds); result.Error != nil { @@ -288,7 +290,7 @@ func (s *BloodhoundDB) UpdateGraphSchemaNodeKind(ctx context.Context, schemaNode RETURNING id, kind_id, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at ) SELECT updated_row.id, %s.name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at - FROM updated_row + FROM updated_row JOIN %s ON %s.id = updated_row.kind_id`, schemaNodeKind.TableName(), kindTable, kindTable, kindTable), schemaNodeKind.SchemaExtensionId, schemaNodeKind.DisplayName, schemaNodeKind.Description, schemaNodeKind.IsDisplayKind, schemaNodeKind.Icon, @@ -436,7 +438,7 @@ func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name strin RETURNING id, kind_id, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at ) SELECT ie.id, ie.schema_extension_id, dk.name, ie.description, ie.is_traversable, ie.created_at, ie.updated_at, ie.deleted_at - FROM inserted_edges ie + FROM inserted_edges ie JOIN dawgs_kind dk ON ie.kind_id = dk.id;`, name, schemaExtensionId, description, isTraversable).Scan(&schemaEdgeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { return schemaEdgeKind, fmt.Errorf("%w: %v", ErrDuplicateSchemaEdgeKindName, result.Error) @@ -457,10 +459,10 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilt if filterAndPagination, err := parseFiltersAndPagination(edgeKindFilters, sort, skip, limit); err != nil { return schemaEdgeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT ek.id, k.name, ek.schema_extension_id, ek.description, ek.is_traversable, + sqlStr := fmt.Sprintf(`SELECT ek.id, k.name, ek.schema_extension_id, ek.description, ek.is_traversable, ek.created_at, ek.updated_at, ek.deleted_at FROM %s ek - JOIN %s k ON ek.kind_id = k.id + JOIN %s k ON ek.kind_id = k.id %s %s %s`, model.GraphSchemaEdgeKind{}.TableName(), kindTable, filterAndPagination.WhereClause, filterAndPagination.OrderSql, filterAndPagination.SkipLimit) @@ -542,7 +544,7 @@ func (s *BloodhoundDB) UpdateGraphSchemaEdgeKind(ctx context.Context, schemaEdge SET schema_extension_id = ?, description = ?, is_traversable = ?, updated_at = NOW() WHERE id = ? RETURNING id, kind_id, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at - ) + ) SELECT updated_row.id, %s.name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at FROM updated_row JOIN %s ON %s.id = updated_row.kind_id`, @@ -570,8 +572,8 @@ func (s *BloodhoundDB) DeleteGraphSchemaEdgeKind(ctx context.Context, schemaEdge return nil } -// CreateSchemaEnvironment - creates a new schema_environment. -func (s *BloodhoundDB) CreateSchemaEnvironment(ctx context.Context, extensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error) { +// CreateEnvironment - creates a new schema_environment. +func (s *BloodhoundDB) CreateEnvironment(ctx context.Context, extensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error) { var schemaEnvironment model.SchemaEnvironment if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` @@ -588,14 +590,30 @@ func (s *BloodhoundDB) CreateSchemaEnvironment(ctx context.Context, extensionId return schemaEnvironment, nil } -// GetSchemaEnvironments - retrieves list of schema environments. -func (s *BloodhoundDB) GetSchemaEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) { +// GetEnvironments - retrieves list of schema environments. +func (s *BloodhoundDB) GetEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) { var result []model.SchemaEnvironment return result, CheckError(s.db.WithContext(ctx).Find(&result)) } -// GetSchemaEnvironmentById - retrieves a schema environment by id. -func (s *BloodhoundDB) GetSchemaEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error) { +// 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 if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` @@ -611,8 +629,8 @@ func (s *BloodhoundDB) GetSchemaEnvironmentById(ctx context.Context, environment return schemaEnvironment, nil } -// DeleteSchemaEnvironment - deletes a schema environment by id. -func (s *BloodhoundDB) DeleteSchemaEnvironment(ctx context.Context, environmentId int32) error { +// DeleteEnvironment - deletes a schema environment by id. +func (s *BloodhoundDB) DeleteEnvironment(ctx context.Context, environmentId int32) error { var schemaEnvironment model.SchemaEnvironment if result := s.db.WithContext(ctx).Exec(fmt.Sprintf(`DELETE FROM %s WHERE id = ?`, schemaEnvironment.TableName()), environmentId); result.Error != nil { @@ -767,7 +785,7 @@ func (s *BloodhoundDB) DeleteRemediation(ctx context.Context, findingId int32) e return nil } -func (s *BloodhoundDB) CreateSchemaEnvironmentPrincipalKind(ctx context.Context, environmentId int32, principalKind int32) (model.SchemaEnvironmentPrincipalKind, error) { +func (s *BloodhoundDB) CreatePrincipalKind(ctx context.Context, environmentId int32, principalKind int32) (model.SchemaEnvironmentPrincipalKind, error) { var envPrincipalKind model.SchemaEnvironmentPrincipalKind if result := s.db.WithContext(ctx).Raw(` @@ -781,7 +799,8 @@ func (s *BloodhoundDB) CreateSchemaEnvironmentPrincipalKind(ctx context.Context, return envPrincipalKind, nil } -func (s *BloodhoundDB) GetSchemaEnvironmentPrincipalKindsByEnvironmentId(ctx context.Context, environmentId int32) (model.SchemaEnvironmentPrincipalKinds, error) { +// GetPrincipalKindsByEnvironmentID - retrieves a schema environments principal kind by environment id. +func (s *BloodhoundDB) GetPrincipalKindsByEnvironmentId(ctx context.Context, environmentId int32) (model.SchemaEnvironmentPrincipalKinds, error) { var envPrincipalKinds model.SchemaEnvironmentPrincipalKinds if result := s.db.WithContext(ctx).Raw(` @@ -795,7 +814,7 @@ func (s *BloodhoundDB) GetSchemaEnvironmentPrincipalKindsByEnvironmentId(ctx con return envPrincipalKinds, nil } -func (s *BloodhoundDB) DeleteSchemaEnvironmentPrincipalKind(ctx context.Context, environmentId int32, principalKind int32) error { +func (s *BloodhoundDB) DeletePrincipalKind(ctx context.Context, environmentId int32, principalKind int32) error { if result := s.db.WithContext(ctx).Exec(` DELETE FROM schema_environments_principal_kinds WHERE environment_id = ? AND principal_kind = ?`, diff --git a/cmd/api/src/database/graphschema_integration_test.go b/cmd/api/src/database/graphschema_integration_test.go index 28db314d6ae..b4431b7d8ff 100644 --- a/cmd/api/src/database/graphschema_integration_test.go +++ b/cmd/api/src/database/graphschema_integration_test.go @@ -1213,7 +1213,7 @@ func TestCreateSchemaEnvironment(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - got, err := testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, testCase.args.extensionId, testCase.args.environmentKindId, testCase.args.sourceKindId) + got, err := testSuite.BHDatabase.CreateEnvironment(testSuite.Context, testCase.args.extensionId, testCase.args.environmentKindId, testCase.args.sourceKindId) if testCase.want.err != nil { assert.EqualError(t, err, testCase.want.err.Error()) } else { @@ -1261,7 +1261,7 @@ func TestGetSchemaEnvironments(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "Extension1", "DisplayName", "v1.0.0") require.NoError(t, err) // Create Environments - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) require.NoError(t, err) return testSuite @@ -1289,9 +1289,9 @@ func TestGetSchemaEnvironments(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "Extension1", "DisplayName", "v1.0.0") require.NoError(t, err) // Create Environments - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(2), int32(2)) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(2), int32(2)) require.NoError(t, err) return testSuite @@ -1328,7 +1328,7 @@ func TestGetSchemaEnvironments(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - got, err := testSuite.BHDatabase.GetSchemaEnvironments(testSuite.Context) + got, err := testSuite.BHDatabase.GetEnvironments(testSuite.Context) if testCase.want.err != nil { assert.EqualError(t, err, testCase.want.err.Error()) } else { @@ -1372,7 +1372,7 @@ func TestGetSchemaEnvironmentById(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "Extension1", "DisplayName", "v1.0.0") require.NoError(t, err) // Create Environment - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) require.NoError(t, err) return testSuite @@ -1409,7 +1409,7 @@ func TestGetSchemaEnvironmentById(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - got, err := testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, testCase.args.environmentId) + got, err := testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, testCase.args.environmentId) if testCase.want.err != nil { assert.ErrorIs(t, err, testCase.want.err) } else { @@ -1451,7 +1451,7 @@ func TestDeleteSchemaEnvironment(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "Extension1", "DisplayName", "v1.0.0") require.NoError(t, err) // Create Environment - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, defaultSchemaExtensionID, int32(1), int32(1)) require.NoError(t, err) return testSuite @@ -1481,14 +1481,14 @@ func TestDeleteSchemaEnvironment(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - err := testSuite.BHDatabase.DeleteSchemaEnvironment(testSuite.Context, testCase.args.environmentId) + err := testSuite.BHDatabase.DeleteEnvironment(testSuite.Context, testCase.args.environmentId) if testCase.want.err != nil { assert.ErrorIs(t, err, testCase.want.err) } else { assert.NoError(t, err) // Verify deletion by trying to get the environment - _, err = testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, testCase.args.environmentId) + _, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, testCase.args.environmentId) assert.ErrorIs(t, err, database.ErrNotFound) } }) @@ -1506,21 +1506,21 @@ func TestTransaction_SchemaEnvironment(t *testing.T) { // Create two environments in a single transaction err = testSuite.BHDatabase.Transaction(testSuite.Context, func(tx *database.BloodhoundDB) error { - _, err := tx.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err := tx.CreateEnvironment(testSuite.Context, 1, 1, 1) if err != nil { return err } - _, err = tx.CreateSchemaEnvironment(testSuite.Context, 1, 2, 2) + _, err = tx.CreateEnvironment(testSuite.Context, 1, 2, 2) return err }) require.NoError(t, err) // Verify both environments were created - env1, err := testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, 1) + env1, err := testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, 1) require.NoError(t, err) assert.Equal(t, int32(1), env1.EnvironmentKindId) - env2, err := testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, 2) + env2, err := testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, 2) require.NoError(t, err) assert.Equal(t, int32(2), env2.EnvironmentKindId) }) @@ -1536,7 +1536,7 @@ func TestTransaction_SchemaEnvironment(t *testing.T) { // Create one environment, then fail - should rollback expectedErr := fmt.Errorf("intentional error to trigger rollback") err = testSuite.BHDatabase.Transaction(testSuite.Context, func(tx *database.BloodhoundDB) error { - _, err := tx.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err := tx.CreateEnvironment(testSuite.Context, 1, 1, 1) if err != nil { return err } @@ -1545,7 +1545,7 @@ func TestTransaction_SchemaEnvironment(t *testing.T) { require.ErrorIs(t, err, expectedErr) // Verify the environment was NOT created (rolled back) - _, err = testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, 1) + _, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, 1) assert.ErrorIs(t, err, database.ErrNotFound) }) @@ -1559,18 +1559,18 @@ func TestTransaction_SchemaEnvironment(t *testing.T) { // Create one environment, then try to create a duplicate - should rollback both err = testSuite.BHDatabase.Transaction(testSuite.Context, func(tx *database.BloodhoundDB) error { - _, err := tx.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err := tx.CreateEnvironment(testSuite.Context, 1, 1, 1) if err != nil { return err } // Try to create duplicate (same environment_kind_id + source_kind_id) - will fail - _, err = tx.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = tx.CreateEnvironment(testSuite.Context, 1, 1, 1) return err }) require.Error(t, err) // Verify the first environment was NOT created (rolled back due to second failure) - _, err = testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, 1) + _, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, 1) assert.ErrorIs(t, err, database.ErrNotFound) }) @@ -1584,16 +1584,16 @@ func TestTransaction_SchemaEnvironment(t *testing.T) { // Create and delete in same transaction err = testSuite.BHDatabase.Transaction(testSuite.Context, func(tx *database.BloodhoundDB) error { - env, err := tx.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + env, err := tx.CreateEnvironment(testSuite.Context, 1, 1, 1) if err != nil { return err } - return tx.DeleteSchemaEnvironment(testSuite.Context, env.ID) + return tx.DeleteEnvironment(testSuite.Context, env.ID) }) require.NoError(t, err) // Verify the environment does not exist - _, err = testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, 1) + _, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, 1) assert.ErrorIs(t, err, database.ErrNotFound) }) } @@ -1625,7 +1625,7 @@ func TestCreateSchemaRelationshipFinding(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "FindingExtension", "Finding Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) return testSuite @@ -1657,7 +1657,7 @@ func TestCreateSchemaRelationshipFinding(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "FindingExtension2", "Finding Extension 2", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "DuplicateName", "Display Name") @@ -1724,7 +1724,7 @@ func TestGetSchemaRelationshipFindingById(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "GetFindingExt", "Get Finding Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "GetByIdFinding", "Get By ID Finding") @@ -1798,7 +1798,7 @@ func TestDeleteSchemaRelationshipFinding(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "DeleteFindingExt", "Delete Finding Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "DeleteFinding", "Delete Finding") @@ -1870,7 +1870,7 @@ func TestCreateRemediation(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "RemediationExt", "Remediation Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "RemediationFinding", "Remediation Finding") @@ -1942,7 +1942,7 @@ func TestGetRemediationByFindingId(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "GetRemediationExt", "Get Remediation Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "GetRemediationFinding", "Get Remediation Finding") @@ -2022,7 +2022,7 @@ func TestUpdateRemediation(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "UpdateRemediationExt", "Update Remediation Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "UpdateRemediationFinding", "Update Remediation Finding") @@ -2059,7 +2059,7 @@ func TestUpdateRemediation(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "UpsertRemediationExt", "Upsert Remediation Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "UpsertRemediationFinding", "Upsert Remediation Finding") @@ -2130,7 +2130,7 @@ func TestDeleteRemediation(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "DeleteRemediationExt", "Delete Remediation Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) _, err = testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, 1, 1, 1, "DeleteRemediationFinding", "Delete Remediation Finding") @@ -2202,7 +2202,7 @@ func TestCreateSchemaEnvironmentPrincipalKind(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "EnvPrincipalKindExt", "Env Principal Kind Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) return testSuite @@ -2224,7 +2224,7 @@ func TestCreateSchemaEnvironmentPrincipalKind(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - result, err := testSuite.BHDatabase.CreateSchemaEnvironmentPrincipalKind(testSuite.Context, testCase.args.environmentId, testCase.args.principalKind) + result, err := testSuite.BHDatabase.CreatePrincipalKind(testSuite.Context, testCase.args.environmentId, testCase.args.principalKind) if testCase.want.err != nil { assert.ErrorIs(t, err, testCase.want.err) } else { @@ -2259,13 +2259,13 @@ func TestGetSchemaEnvironmentPrincipalKindsByEnvironmentId(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "GetEnvPrincipalKindExt", "Get Env Principal Kind Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironmentPrincipalKind(testSuite.Context, 1, 1) + _, err = testSuite.BHDatabase.CreatePrincipalKind(testSuite.Context, 1, 1) require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironmentPrincipalKind(testSuite.Context, 1, 2) + _, err = testSuite.BHDatabase.CreatePrincipalKind(testSuite.Context, 1, 2) require.NoError(t, err) return testSuite @@ -2295,7 +2295,7 @@ func TestGetSchemaEnvironmentPrincipalKindsByEnvironmentId(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - result, err := testSuite.BHDatabase.GetSchemaEnvironmentPrincipalKindsByEnvironmentId(testSuite.Context, testCase.args.environmentId) + result, err := testSuite.BHDatabase.GetPrincipalKindsByEnvironmentId(testSuite.Context, testCase.args.environmentId) if testCase.want.err != nil { assert.ErrorIs(t, err, testCase.want.err) } else { @@ -2329,10 +2329,10 @@ func TestDeleteSchemaEnvironmentPrincipalKind(t *testing.T) { _, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "DeleteEnvPrincipalKindExt", "Delete Env Principal Kind Extension", "v1.0.0") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, 1, 1, 1) + _, err = testSuite.BHDatabase.CreateEnvironment(testSuite.Context, 1, 1, 1) require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironmentPrincipalKind(testSuite.Context, 1, 1) + _, err = testSuite.BHDatabase.CreatePrincipalKind(testSuite.Context, 1, 1) require.NoError(t, err) return testSuite @@ -2364,12 +2364,12 @@ func TestDeleteSchemaEnvironmentPrincipalKind(t *testing.T) { testSuite := testCase.setup() defer teardownIntegrationTestSuite(t, &testSuite) - err := testSuite.BHDatabase.DeleteSchemaEnvironmentPrincipalKind(testSuite.Context, testCase.args.environmentId, testCase.args.principalKind) + err := testSuite.BHDatabase.DeletePrincipalKind(testSuite.Context, testCase.args.environmentId, testCase.args.principalKind) if testCase.want.err != nil { assert.ErrorIs(t, err, testCase.want.err) } else { assert.NoError(t, err) - result, err := testSuite.BHDatabase.GetSchemaEnvironmentPrincipalKindsByEnvironmentId(testSuite.Context, testCase.args.environmentId) + result, err := testSuite.BHDatabase.GetPrincipalKindsByEnvironmentId(testSuite.Context, testCase.args.environmentId) assert.NoError(t, err) assert.Len(t, result, 0) } @@ -2394,7 +2394,7 @@ func TestDeleteSchemaExtension_CascadeDeletesAllDependents(t *testing.T) { edgeKind, err := testSuite.BHDatabase.CreateGraphSchemaEdgeKind(testSuite.Context, "CascadeTestEdgeKind", extension.ID, "Test description", true) require.NoError(t, err) - environment, err := testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, extension.ID, nodeKind.ID, nodeKind.ID) + environment, err := testSuite.BHDatabase.CreateEnvironment(testSuite.Context, extension.ID, nodeKind.ID, nodeKind.ID) require.NoError(t, err) relationshipFinding, err := testSuite.BHDatabase.CreateSchemaRelationshipFinding(testSuite.Context, extension.ID, edgeKind.ID, environment.ID, "CascadeTestFinding", "Cascade Test Finding") @@ -2403,7 +2403,7 @@ func TestDeleteSchemaExtension_CascadeDeletesAllDependents(t *testing.T) { _, err = testSuite.BHDatabase.CreateRemediation(testSuite.Context, relationshipFinding.ID, "Short desc", "Long desc", "Short remediation", "Long remediation") require.NoError(t, err) - _, err = testSuite.BHDatabase.CreateSchemaEnvironmentPrincipalKind(testSuite.Context, environment.ID, nodeKind.ID) + _, err = testSuite.BHDatabase.CreatePrincipalKind(testSuite.Context, environment.ID, nodeKind.ID) require.NoError(t, err) err = testSuite.BHDatabase.DeleteGraphSchemaExtension(testSuite.Context, extension.ID) @@ -2418,7 +2418,7 @@ func TestDeleteSchemaExtension_CascadeDeletesAllDependents(t *testing.T) { _, err = testSuite.BHDatabase.GetGraphSchemaEdgeKindById(testSuite.Context, edgeKind.ID) assert.ErrorIs(t, err, database.ErrNotFound) - _, err = testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, environment.ID) + _, err = testSuite.BHDatabase.GetEnvironmentById(testSuite.Context, environment.ID) assert.ErrorIs(t, err, database.ErrNotFound) _, err = testSuite.BHDatabase.GetSchemaRelationshipFindingById(testSuite.Context, relationshipFinding.ID) @@ -2427,7 +2427,7 @@ func TestDeleteSchemaExtension_CascadeDeletesAllDependents(t *testing.T) { _, err = testSuite.BHDatabase.GetRemediationByFindingId(testSuite.Context, relationshipFinding.ID) assert.ErrorIs(t, err, database.ErrNotFound) - principalKinds, err := testSuite.BHDatabase.GetSchemaEnvironmentPrincipalKindsByEnvironmentId(testSuite.Context, environment.ID) + principalKinds, err := testSuite.BHDatabase.GetPrincipalKindsByEnvironmentId(testSuite.Context, environment.ID) assert.NoError(t, err) assert.Len(t, principalKinds, 0) } diff --git a/cmd/api/src/database/kind.go b/cmd/api/src/database/kind.go new file mode 100644 index 00000000000..0487f28375d --- /dev/null +++ b/cmd/api/src/database/kind.go @@ -0,0 +1,47 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package database + +import ( + "context" + + "github.com/specterops/bloodhound/cmd/api/src/model" +) + +type Kind interface { + GetKindByName(ctx context.Context, name string) (model.Kind, error) +} + +func (s *BloodhoundDB) GetKindByName(ctx context.Context, name string) (model.Kind, error) { + const query = ` + SELECT id, name + FROM kind + WHERE name = $1; + ` + + var kind model.Kind + result := s.db.WithContext(ctx).Raw(query, name).Scan(&kind) + + if result.Error != nil { + return model.Kind{}, result.Error + } + + if result.RowsAffected == 0 || kind.ID == 0 { + return model.Kind{}, ErrNotFound + } + + return kind, nil +} diff --git a/cmd/api/src/database/kind_integration_test.go b/cmd/api/src/database/kind_integration_test.go new file mode 100644 index 00000000000..8c541dfc55a --- /dev/null +++ b/cmd/api/src/database/kind_integration_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build integration + +package database_test + +import ( + "testing" + + "github.com/specterops/bloodhound/cmd/api/src/model" + "github.com/stretchr/testify/assert" +) + +// the v7.3.0 migration initializes the kind table with Tag_Tier_Zero, so we're +// simply testing the kind exists +func TestGetKindByName(t *testing.T) { + type args struct { + name string + } + type want struct { + err error + kind model.Kind + } + tests := []struct { + name string + args args + setup func() IntegrationTestSuite + want want + }{ + { + name: "Success: Retrieves Kind Tag_Tier_Zero by name", + args: args{ + name: "Tag_Tier_Zero", + }, + setup: func() IntegrationTestSuite { + return setupIntegrationTestSuite(t) + }, + want: want{ + err: nil, + kind: model.Kind{ + ID: 1, + Name: "Tag_Tier_Zero", + }, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + testSuite := testCase.setup() + defer teardownIntegrationTestSuite(t, &testSuite) + + kind, err := testSuite.BHDatabase.GetKindByName(testSuite.Context, testCase.args.name) + if testCase.want.err != nil { + assert.EqualError(t, err, testCase.want.err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.want.kind, kind) + } + }) + } +} diff --git a/cmd/api/src/database/migration/migrations/v8.6.0.sql b/cmd/api/src/database/migration/migrations/v8.6.0.sql new file mode 100644 index 00000000000..219f6bb54d0 --- /dev/null +++ b/cmd/api/src/database/migration/migrations/v8.6.0.sql @@ -0,0 +1,22 @@ +-- Copyright 2026 Specter Ops, Inc. +-- +-- Licensed under the Apache License, Version 2.0 +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- SPDX-License-Identifier: Apache-2.0 + +ALTER TABLE IF EXISTS schema_environments + DROP CONSTRAINT IF EXISTS schema_environments_source_kind_id_fkey; + +ALTER TABLE IF EXISTS schema_environments + ADD CONSTRAINT schema_environments_source_kind_id_fkey + FOREIGN KEY (source_kind_id) REFERENCES source_kinds(id); diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index cf16fece304..c23d6e150f5 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -341,6 +341,21 @@ func (mr *MockDatabaseMockRecorder) CreateCustomNodeKinds(ctx, customNodeKind an return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCustomNodeKinds", reflect.TypeOf((*MockDatabase)(nil).CreateCustomNodeKinds), ctx, customNodeKind) } +// CreateEnvironment mocks base method. +func (m *MockDatabase) CreateEnvironment(ctx context.Context, extensionId, environmentKindId, sourceKindId int32) (model.SchemaEnvironment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateEnvironment", ctx, extensionId, environmentKindId, sourceKindId) + ret0, _ := ret[0].(model.SchemaEnvironment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateEnvironment indicates an expected call of CreateEnvironment. +func (mr *MockDatabaseMockRecorder) CreateEnvironment(ctx, extensionId, environmentKindId, sourceKindId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEnvironment", reflect.TypeOf((*MockDatabase)(nil).CreateEnvironment), ctx, extensionId, environmentKindId, sourceKindId) +} + // CreateGraphSchemaEdgeKind mocks base method. func (m *MockDatabase) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { m.ctrl.T.Helper() @@ -461,6 +476,21 @@ func (mr *MockDatabaseMockRecorder) CreateOIDCProvider(ctx, name, issuer, client return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOIDCProvider", reflect.TypeOf((*MockDatabase)(nil).CreateOIDCProvider), ctx, name, issuer, clientID, config) } +// CreatePrincipalKind mocks base method. +func (m *MockDatabase) CreatePrincipalKind(ctx context.Context, environmentId, principalKind int32) (model.SchemaEnvironmentPrincipalKind, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePrincipalKind", ctx, environmentId, principalKind) + ret0, _ := ret[0].(model.SchemaEnvironmentPrincipalKind) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePrincipalKind indicates an expected call of CreatePrincipalKind. +func (mr *MockDatabaseMockRecorder) CreatePrincipalKind(ctx, environmentId, principalKind any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePrincipalKind", reflect.TypeOf((*MockDatabase)(nil).CreatePrincipalKind), ctx, environmentId, principalKind) +} + // CreateRemediation mocks base method. func (m *MockDatabase) CreateRemediation(ctx context.Context, findingId int32, shortDescription, longDescription, shortRemediation, longRemediation string) (model.Remediation, error) { m.ctrl.T.Helper() @@ -570,36 +600,6 @@ func (mr *MockDatabaseMockRecorder) CreateSavedQueryPermissionsToUsers(ctx, quer return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSavedQueryPermissionsToUsers", reflect.TypeOf((*MockDatabase)(nil).CreateSavedQueryPermissionsToUsers), varargs...) } -// CreateSchemaEnvironment mocks base method. -func (m *MockDatabase) CreateSchemaEnvironment(ctx context.Context, extensionId, environmentKindId, sourceKindId int32) (model.SchemaEnvironment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSchemaEnvironment", ctx, extensionId, environmentKindId, sourceKindId) - ret0, _ := ret[0].(model.SchemaEnvironment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateSchemaEnvironment indicates an expected call of CreateSchemaEnvironment. -func (mr *MockDatabaseMockRecorder) CreateSchemaEnvironment(ctx, extensionId, environmentKindId, sourceKindId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSchemaEnvironment", reflect.TypeOf((*MockDatabase)(nil).CreateSchemaEnvironment), ctx, extensionId, environmentKindId, sourceKindId) -} - -// CreateSchemaEnvironmentPrincipalKind mocks base method. -func (m *MockDatabase) CreateSchemaEnvironmentPrincipalKind(ctx context.Context, environmentId, principalKind int32) (model.SchemaEnvironmentPrincipalKind, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSchemaEnvironmentPrincipalKind", ctx, environmentId, principalKind) - ret0, _ := ret[0].(model.SchemaEnvironmentPrincipalKind) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateSchemaEnvironmentPrincipalKind indicates an expected call of CreateSchemaEnvironmentPrincipalKind. -func (mr *MockDatabaseMockRecorder) CreateSchemaEnvironmentPrincipalKind(ctx, environmentId, principalKind any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSchemaEnvironmentPrincipalKind", reflect.TypeOf((*MockDatabase)(nil).CreateSchemaEnvironmentPrincipalKind), ctx, environmentId, principalKind) -} - // CreateSchemaRelationshipFinding mocks base method. func (m *MockDatabase) CreateSchemaRelationshipFinding(ctx context.Context, extensionId, relationshipKindId, environmentId int32, name, displayName string) (model.SchemaRelationshipFinding, error) { m.ctrl.T.Helper() @@ -842,6 +842,20 @@ func (mr *MockDatabaseMockRecorder) DeleteCustomNodeKind(ctx, kindName any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCustomNodeKind", reflect.TypeOf((*MockDatabase)(nil).DeleteCustomNodeKind), ctx, kindName) } +// DeleteEnvironment mocks base method. +func (m *MockDatabase) DeleteEnvironment(ctx context.Context, environmentId int32) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteEnvironment", ctx, environmentId) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteEnvironment indicates an expected call of DeleteEnvironment. +func (mr *MockDatabaseMockRecorder) DeleteEnvironment(ctx, environmentId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEnvironment", reflect.TypeOf((*MockDatabase)(nil).DeleteEnvironment), ctx, environmentId) +} + // DeleteEnvironmentTargetedAccessControlForUser mocks base method. func (m *MockDatabase) DeleteEnvironmentTargetedAccessControlForUser(ctx context.Context, user model.User) error { m.ctrl.T.Helper() @@ -926,6 +940,20 @@ func (mr *MockDatabaseMockRecorder) DeleteIngestTask(ctx, ingestTask any) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIngestTask", reflect.TypeOf((*MockDatabase)(nil).DeleteIngestTask), ctx, ingestTask) } +// DeletePrincipalKind mocks base method. +func (m *MockDatabase) DeletePrincipalKind(ctx context.Context, environmentId, principalKind int32) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePrincipalKind", ctx, environmentId, principalKind) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePrincipalKind indicates an expected call of DeletePrincipalKind. +func (mr *MockDatabaseMockRecorder) DeletePrincipalKind(ctx, environmentId, principalKind any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePrincipalKind", reflect.TypeOf((*MockDatabase)(nil).DeletePrincipalKind), ctx, environmentId, principalKind) +} + // DeleteRemediation mocks base method. func (m *MockDatabase) DeleteRemediation(ctx context.Context, findingId int32) error { m.ctrl.T.Helper() @@ -987,34 +1015,6 @@ func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionsForUsers(ctx, que return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionsForUsers", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionsForUsers), varargs...) } -// DeleteSchemaEnvironment mocks base method. -func (m *MockDatabase) DeleteSchemaEnvironment(ctx context.Context, environmentId int32) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSchemaEnvironment", ctx, environmentId) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSchemaEnvironment indicates an expected call of DeleteSchemaEnvironment. -func (mr *MockDatabaseMockRecorder) DeleteSchemaEnvironment(ctx, environmentId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSchemaEnvironment", reflect.TypeOf((*MockDatabase)(nil).DeleteSchemaEnvironment), ctx, environmentId) -} - -// DeleteSchemaEnvironmentPrincipalKind mocks base method. -func (m *MockDatabase) DeleteSchemaEnvironmentPrincipalKind(ctx context.Context, environmentId, principalKind int32) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSchemaEnvironmentPrincipalKind", ctx, environmentId, principalKind) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSchemaEnvironmentPrincipalKind indicates an expected call of DeleteSchemaEnvironmentPrincipalKind. -func (mr *MockDatabaseMockRecorder) DeleteSchemaEnvironmentPrincipalKind(ctx, environmentId, principalKind any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSchemaEnvironmentPrincipalKind", reflect.TypeOf((*MockDatabase)(nil).DeleteSchemaEnvironmentPrincipalKind), ctx, environmentId, principalKind) -} - // DeleteSchemaRelationshipFinding mocks base method. func (m *MockDatabase) DeleteSchemaRelationshipFinding(ctx context.Context, findingId int32) error { m.ctrl.T.Helper() @@ -1702,6 +1702,36 @@ func (mr *MockDatabaseMockRecorder) GetDatapipeStatus(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDatapipeStatus", reflect.TypeOf((*MockDatabase)(nil).GetDatapipeStatus), ctx) } +// GetEnvironmentById mocks base method. +func (m *MockDatabase) GetEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnvironmentById", ctx, environmentId) + ret0, _ := ret[0].(model.SchemaEnvironment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironmentById indicates an expected call of GetEnvironmentById. +func (mr *MockDatabaseMockRecorder) GetEnvironmentById(ctx, environmentId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentById", reflect.TypeOf((*MockDatabase)(nil).GetEnvironmentById), ctx, environmentId) +} + +// GetEnvironmentByKinds mocks base method. +func (m *MockDatabase) GetEnvironmentByKinds(ctx context.Context, environmentKindId, sourceKindId int32) (model.SchemaEnvironment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnvironmentByKinds", ctx, environmentKindId, sourceKindId) + ret0, _ := ret[0].(model.SchemaEnvironment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironmentByKinds indicates an expected call of GetEnvironmentByKinds. +func (mr *MockDatabaseMockRecorder) GetEnvironmentByKinds(ctx, environmentKindId, sourceKindId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentByKinds", reflect.TypeOf((*MockDatabase)(nil).GetEnvironmentByKinds), ctx, environmentKindId, sourceKindId) +} + // GetEnvironmentTargetedAccessControlForUser mocks base method. func (m *MockDatabase) GetEnvironmentTargetedAccessControlForUser(ctx context.Context, user model.User) ([]model.EnvironmentTargetedAccessControl, error) { m.ctrl.T.Helper() @@ -1717,6 +1747,21 @@ func (mr *MockDatabaseMockRecorder) GetEnvironmentTargetedAccessControlForUser(c return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentTargetedAccessControlForUser", reflect.TypeOf((*MockDatabase)(nil).GetEnvironmentTargetedAccessControlForUser), ctx, user) } +// GetEnvironments mocks base method. +func (m *MockDatabase) GetEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEnvironments", ctx) + ret0, _ := ret[0].([]model.SchemaEnvironment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironments indicates an expected call of GetEnvironments. +func (mr *MockDatabaseMockRecorder) GetEnvironments(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironments", reflect.TypeOf((*MockDatabase)(nil).GetEnvironments), ctx) +} + // GetFlag mocks base method. func (m *MockDatabase) GetFlag(ctx context.Context, id int32) (appcfg.FeatureFlag, error) { m.ctrl.T.Helper() @@ -1947,6 +1992,21 @@ func (mr *MockDatabaseMockRecorder) GetInstallation(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstallation", reflect.TypeOf((*MockDatabase)(nil).GetInstallation), ctx) } +// GetKindByName mocks base method. +func (m *MockDatabase) GetKindByName(ctx context.Context, name string) (model.Kind, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKindByName", ctx, name) + ret0, _ := ret[0].(model.Kind) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKindByName indicates an expected call of GetKindByName. +func (mr *MockDatabaseMockRecorder) GetKindByName(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKindByName", reflect.TypeOf((*MockDatabase)(nil).GetKindByName), ctx, name) +} + // GetLatestAssetGroupCollection mocks base method. func (m *MockDatabase) GetLatestAssetGroupCollection(ctx context.Context, assetGroupID int32) (model.AssetGroupCollection, error) { m.ctrl.T.Helper() @@ -1992,6 +2052,21 @@ func (mr *MockDatabaseMockRecorder) GetPermission(ctx, id any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermission", reflect.TypeOf((*MockDatabase)(nil).GetPermission), ctx, id) } +// GetPrincipalKindsByEnvironmentId mocks base method. +func (m *MockDatabase) GetPrincipalKindsByEnvironmentId(ctx context.Context, environmentId int32) (model.SchemaEnvironmentPrincipalKinds, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPrincipalKindsByEnvironmentId", ctx, environmentId) + ret0, _ := ret[0].(model.SchemaEnvironmentPrincipalKinds) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPrincipalKindsByEnvironmentId indicates an expected call of GetPrincipalKindsByEnvironmentId. +func (mr *MockDatabaseMockRecorder) GetPrincipalKindsByEnvironmentId(ctx, environmentId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPrincipalKindsByEnvironmentId", reflect.TypeOf((*MockDatabase)(nil).GetPrincipalKindsByEnvironmentId), ctx, environmentId) +} + // GetPublicSavedQueries mocks base method. func (m *MockDatabase) GetPublicSavedQueries(ctx context.Context) (model.SavedQueries, error) { m.ctrl.T.Helper() @@ -2172,51 +2247,6 @@ func (mr *MockDatabaseMockRecorder) GetSavedQueryPermissions(ctx, queryID any) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSavedQueryPermissions", reflect.TypeOf((*MockDatabase)(nil).GetSavedQueryPermissions), ctx, queryID) } -// GetSchemaEnvironmentById mocks base method. -func (m *MockDatabase) GetSchemaEnvironmentById(ctx context.Context, environmentId int32) (model.SchemaEnvironment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSchemaEnvironmentById", ctx, environmentId) - ret0, _ := ret[0].(model.SchemaEnvironment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSchemaEnvironmentById indicates an expected call of GetSchemaEnvironmentById. -func (mr *MockDatabaseMockRecorder) GetSchemaEnvironmentById(ctx, environmentId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaEnvironmentById", reflect.TypeOf((*MockDatabase)(nil).GetSchemaEnvironmentById), ctx, environmentId) -} - -// GetSchemaEnvironmentPrincipalKindsByEnvironmentId mocks base method. -func (m *MockDatabase) GetSchemaEnvironmentPrincipalKindsByEnvironmentId(ctx context.Context, environmentId int32) (model.SchemaEnvironmentPrincipalKinds, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSchemaEnvironmentPrincipalKindsByEnvironmentId", ctx, environmentId) - ret0, _ := ret[0].(model.SchemaEnvironmentPrincipalKinds) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSchemaEnvironmentPrincipalKindsByEnvironmentId indicates an expected call of GetSchemaEnvironmentPrincipalKindsByEnvironmentId. -func (mr *MockDatabaseMockRecorder) GetSchemaEnvironmentPrincipalKindsByEnvironmentId(ctx, environmentId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaEnvironmentPrincipalKindsByEnvironmentId", reflect.TypeOf((*MockDatabase)(nil).GetSchemaEnvironmentPrincipalKindsByEnvironmentId), ctx, environmentId) -} - -// GetSchemaEnvironments mocks base method. -func (m *MockDatabase) GetSchemaEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSchemaEnvironments", ctx) - ret0, _ := ret[0].([]model.SchemaEnvironment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSchemaEnvironments indicates an expected call of GetSchemaEnvironments. -func (mr *MockDatabaseMockRecorder) GetSchemaEnvironments(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchemaEnvironments", reflect.TypeOf((*MockDatabase)(nil).GetSchemaEnvironments), ctx) -} - // GetSchemaRelationshipFindingById mocks base method. func (m *MockDatabase) GetSchemaRelationshipFindingById(ctx context.Context, findingId int32) (model.SchemaRelationshipFinding, error) { m.ctrl.T.Helper() @@ -2318,6 +2348,21 @@ func (mr *MockDatabaseMockRecorder) GetSharedSavedQueries(ctx, userID any) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharedSavedQueries", reflect.TypeOf((*MockDatabase)(nil).GetSharedSavedQueries), ctx, userID) } +// GetSourceKindByName mocks base method. +func (m *MockDatabase) GetSourceKindByName(ctx context.Context, name string) (database.SourceKind, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSourceKindByName", ctx, name) + ret0, _ := ret[0].(database.SourceKind) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSourceKindByName indicates an expected call of GetSourceKindByName. +func (mr *MockDatabaseMockRecorder) GetSourceKindByName(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSourceKindByName", reflect.TypeOf((*MockDatabase)(nil).GetSourceKindByName), ctx, name) +} + // GetSourceKinds mocks base method. func (m *MockDatabase) GetSourceKinds(ctx context.Context) ([]database.SourceKind, error) { m.ctrl.T.Helper() diff --git a/cmd/api/src/database/sourcekinds.go b/cmd/api/src/database/sourcekinds.go index bf95599dd8d..6aa76b8a24a 100644 --- a/cmd/api/src/database/sourcekinds.go +++ b/cmd/api/src/database/sourcekinds.go @@ -28,6 +28,7 @@ type SourceKindsData interface { GetSourceKinds(ctx context.Context) ([]SourceKind, error) DeactivateSourceKindsByName(ctx context.Context, kinds graph.Kinds) error RegisterSourceKind(ctx context.Context) func(sourceKind graph.Kind) error + GetSourceKindByName(ctx context.Context, name string) (SourceKind, error) } // RegisterSourceKind returns a function that inserts a source kind by name, @@ -93,6 +94,39 @@ func (s *BloodhoundDB) GetSourceKinds(ctx context.Context) ([]SourceKind, error) return out, nil } +func (s *BloodhoundDB) GetSourceKindByName(ctx context.Context, name string) (SourceKind, error) { + const query = ` + SELECT id, name, active + FROM source_kinds + WHERE name = $1 AND active = true; + ` + + type rawSourceKind struct { + ID int + Name string + Active bool + } + + var raw rawSourceKind + result := s.db.WithContext(ctx).Raw(query, name).Scan(&raw) + + if result.Error != nil { + return SourceKind{}, result.Error + } + + if result.RowsAffected == 0 || raw.ID == 0 { + return SourceKind{}, ErrNotFound + } + + kind := SourceKind{ + ID: raw.ID, + Name: graph.StringKind(raw.Name), + Active: raw.Active, + } + + return kind, nil +} + func (s *BloodhoundDB) DeactivateSourceKindsByName(ctx context.Context, kinds graph.Kinds) error { if len(kinds) == 0 { return nil diff --git a/cmd/api/src/database/sourcekinds_integration_test.go b/cmd/api/src/database/sourcekinds_integration_test.go index 8d6cbbab458..0d8931d504b 100644 --- a/cmd/api/src/database/sourcekinds_integration_test.go +++ b/cmd/api/src/database/sourcekinds_integration_test.go @@ -142,7 +142,7 @@ func TestRegisterSourceKind(t *testing.T) { err := testSuite.BHDatabase.RegisterSourceKind(testSuite.Context)(testCase.args.sourceKind) if testCase.want.err != nil { - assert.EqualError(t, testCase.want.err, err.Error()) + assert.EqualError(t, err, testCase.want.err.Error()) } else { assert.NoError(t, err) } @@ -197,7 +197,7 @@ func TestGetSourceKinds(t *testing.T) { sourceKinds, err := testSuite.BHDatabase.GetSourceKinds(testSuite.Context) if testCase.want.err != nil { - assert.EqualError(t, testCase.want.err, err.Error()) + assert.EqualError(t, err, testCase.want.err.Error()) } else { assert.NoError(t, err) assert.Equal(t, testCase.want.sourceKinds, sourceKinds) @@ -206,6 +206,56 @@ func TestGetSourceKinds(t *testing.T) { } } +func TestGetSourceKindByName(t *testing.T) { + type args struct { + name string + } + type want struct { + err error + sourceKind database.SourceKind + } + tests := []struct { + name string + args args + setup func() IntegrationTestSuite + want want + }{ + { + name: "Success: Retrieves Source Kinds by Name", + args: args{ + name: "AZBase", + }, + setup: func() IntegrationTestSuite { + return setupIntegrationTestSuite(t) + }, + want: want{ + err: nil, + // the v8.0.0 migration initializes the source_kinds table with Base, AZBase, so we're + // simply testing the default returned source_kinds + sourceKind: database.SourceKind{ + ID: 2, + Name: graph.StringKind("AZBase"), + Active: true, + }, + }, + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + testSuite := testCase.setup() + defer teardownIntegrationTestSuite(t, &testSuite) + + sourceKind, err := testSuite.BHDatabase.GetSourceKindByName(testSuite.Context, testCase.args.name) + if testCase.want.err != nil { + assert.EqualError(t, err, testCase.want.err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.want.sourceKind, sourceKind) + } + }) + } +} + func TestDeactivateSourceKindsByName(t *testing.T) { type args struct { sourceKind graph.Kinds @@ -373,7 +423,7 @@ func TestDeactivateSourceKindsByName(t *testing.T) { err := testSuite.BHDatabase.DeactivateSourceKindsByName(testSuite.Context, testCase.args.sourceKind) if testCase.want.err != nil { - assert.EqualError(t, testCase.want.err, err.Error()) + assert.EqualError(t, err, testCase.want.err.Error()) } else { assert.NoError(t, err) } diff --git a/cmd/api/src/database/upsert_schema_environment.go b/cmd/api/src/database/upsert_schema_environment.go new file mode 100644 index 00000000000..180ec7702a5 --- /dev/null +++ b/cmd/api/src/database/upsert_schema_environment.go @@ -0,0 +1,160 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package database + +import ( + "context" + "errors" + "fmt" + + "github.com/specterops/bloodhound/cmd/api/src/model" + "github.com/specterops/dawgs/graph" +) + +// UpsertSchemaEnvironmentWithPrincipalKinds creates or updates an environment with its principal kinds. +// If an environment with the same environment kind and source kind exists, it will be replaced. +func (s *BloodhoundDB) UpsertSchemaEnvironmentWithPrincipalKinds(ctx context.Context, schemaExtensionId int32, environmentKind string, sourceKind string, principalKinds []string) error { + environment := model.SchemaEnvironment{ + SchemaExtensionId: schemaExtensionId, + } + + envKind, err := s.validateAndTranslateEnvironmentKind(ctx, environmentKind) + if err != nil { + return err + } + + sourceKindID, err := s.validateAndTranslateSourceKind(ctx, sourceKind) + if err != nil { + return err + } + + translatedPrincipalKinds, err := s.validateAndTranslatePrincipalKinds(ctx, principalKinds) + if err != nil { + return err + } + + environment.EnvironmentKindId = int32(envKind.ID) + environment.SourceKindId = sourceKindID + + envID, err := s.replaceSchemaEnvironment(ctx, environment) + if err != nil { + return fmt.Errorf("error replacing or creating schema environment: %w", err) + } + + if err := s.replacePrincipalKinds(ctx, envID, translatedPrincipalKinds); err != nil { + return fmt.Errorf("error replacing principal kinds: %w", err) + } + + return nil +} + +// validateAndTranslateEnvironmentKind validates that the environment kind exists in the kinds table. +func (s *BloodhoundDB) validateAndTranslateEnvironmentKind(ctx context.Context, environmentKindName string) (model.Kind, error) { + if envKind, err := s.GetKindByName(ctx, environmentKindName); err != nil && !errors.Is(err, ErrNotFound) { + return model.Kind{}, fmt.Errorf("error retrieving environment kind '%s': %w", environmentKindName, err) + } else if errors.Is(err, ErrNotFound) { + return model.Kind{}, fmt.Errorf("environment kind '%s' not found", environmentKindName) + } else { + return envKind, nil + } +} + +// validateAndTranslateSourceKind validates that the source kind exists in the source_kinds table. +// If not found, it registers the source kind and returns its ID so it can be added to the Environment object. +func (s *BloodhoundDB) validateAndTranslateSourceKind(ctx context.Context, sourceKindName string) (int32, error) { + if sourceKind, err := s.GetSourceKindByName(ctx, sourceKindName); err != nil && !errors.Is(err, ErrNotFound) { + return 0, fmt.Errorf("error retrieving source kind '%s': %w", sourceKindName, err) + } else if err == nil { + return int32(sourceKind.ID), nil + } + + // If source kind is not found, register it. If it exists and is inactive, it will automatically update as active. + kindType := graph.StringKind(sourceKindName) + if err := s.RegisterSourceKind(ctx)(kindType); err != nil { + return 0, fmt.Errorf("error registering source kind '%s': %w", sourceKindName, err) + } + + if sourceKind, err := s.GetSourceKindByName(ctx, sourceKindName); err != nil { + return 0, fmt.Errorf("error retrieving newly registered source kind '%s': %w", sourceKindName, err) + } else { + return int32(sourceKind.ID), nil + } +} + +// validateAndTranslatePrincipalKinds ensures all principalKinds exist in the kinds table. +// It also translates them to IDs so they can be upserted into the database. +func (s *BloodhoundDB) validateAndTranslatePrincipalKinds(ctx context.Context, principalKindNames []string) ([]model.SchemaEnvironmentPrincipalKind, error) { + principalKinds := make([]model.SchemaEnvironmentPrincipalKind, len(principalKindNames)) + + for i, kindName := range principalKindNames { + if kind, err := s.GetKindByName(ctx, kindName); err != nil && !errors.Is(err, ErrNotFound) { + return nil, fmt.Errorf("error retrieving principal kind by name '%s': %w", kindName, err) + } else if errors.Is(err, ErrNotFound) { + return nil, fmt.Errorf("principal kind '%s' not found", kindName) + } else { + principalKinds[i] = model.SchemaEnvironmentPrincipalKind{ + PrincipalKind: int32(kind.ID), + } + } + } + + return principalKinds, nil +} + +// replaceSchemaEnvironment creates or updates a schema environment. +// If an environment with the given kinds exists, it deletes it first before creating the new one. +// The unique constraint on (environment_kind_id, source_kind_id) of the Schema Environment table ensures no +// duplicate pairs exist, enabling this upsert logic. +func (s *BloodhoundDB) replaceSchemaEnvironment(ctx context.Context, graphSchema model.SchemaEnvironment) (int32, error) { + if existing, err := s.GetEnvironmentByKinds(ctx, graphSchema.EnvironmentKindId, graphSchema.SourceKindId); err != nil && !errors.Is(err, ErrNotFound) { + return 0, fmt.Errorf("error retrieving schema environment: %w", err) + } else if !errors.Is(err, ErrNotFound) { + // Environment exists - delete it first + if err := s.DeleteEnvironment(ctx, existing.ID); err != nil { + return 0, fmt.Errorf("error deleting schema environment %d: %w", existing.ID, err) + } + } + + // Create Environment + if created, err := s.CreateEnvironment(ctx, graphSchema.SchemaExtensionId, graphSchema.EnvironmentKindId, graphSchema.SourceKindId); err != nil { + return 0, fmt.Errorf("error creating schema environment: %w", err) + } else { + return created.ID, nil + } +} + +// replacePrincipalKinds deletes all existing principal kinds for an environment and creates new ones. +func (s *BloodhoundDB) replacePrincipalKinds(ctx context.Context, environmentID int32, principalKinds []model.SchemaEnvironmentPrincipalKind) error { + if existingKinds, err := s.GetPrincipalKindsByEnvironmentId(ctx, environmentID); err != nil && !errors.Is(err, ErrNotFound) { + return fmt.Errorf("error retrieving existing principal kinds for environment %d: %w", environmentID, err) + } else if !errors.Is(err, ErrNotFound) { + // Delete all existing principal kinds + for _, kind := range existingKinds { + if err := s.DeletePrincipalKind(ctx, kind.EnvironmentId, kind.PrincipalKind); err != nil { + return fmt.Errorf("error deleting principal kind %d for environment %d: %w", kind.PrincipalKind, kind.EnvironmentId, err) + } + } + } + + // Create the new principal kinds + for _, kind := range principalKinds { + if _, err := s.CreatePrincipalKind(ctx, environmentID, kind.PrincipalKind); err != nil { + return fmt.Errorf("error creating principal kind %d for environment %d: %w", kind.PrincipalKind, environmentID, err) + } + } + + return nil +} diff --git a/cmd/api/src/database/upsert_schema_environment_integration_test.go b/cmd/api/src/database/upsert_schema_environment_integration_test.go new file mode 100644 index 00000000000..41409b4b991 --- /dev/null +++ b/cmd/api/src/database/upsert_schema_environment_integration_test.go @@ -0,0 +1,308 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build integration + +package database_test + +import ( + "context" + "testing" + + "github.com/specterops/bloodhound/cmd/api/src/database" + "github.com/specterops/dawgs/graph" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBloodhoundDB_UpsertSchemaEnvironmentWithPrincipalKinds(t *testing.T) { + type args struct { + environmentKind string + sourceKind string + principalKinds []string + } + tests := []struct { + name string + setupData func(t *testing.T, db *database.BloodhoundDB) int32 + args args + assert func(t *testing.T, db *database.BloodhoundDB) + expectedError string + }{ + { + name: "Success: Create new environment with principal kinds", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "Tag_Tier_Zero", + sourceKind: "Base", + principalKinds: []string{"Tag_Tier_Zero", "Tag_Owned"}, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + expectedPrincipalKindNames := []string{"Tag_Tier_Zero", "Tag_Owned"} + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 1, len(environments)) + + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, len(expectedPrincipalKindNames), len(principalKinds)) + + expectedKindIDs := make([]int32, len(expectedPrincipalKindNames)) + for i, name := range expectedPrincipalKindNames { + kind, err := db.GetKindByName(context.Background(), name) + assert.NoError(t, err) + expectedKindIDs[i] = int32(kind.ID) + } + + actualKindIDs := make([]int32, len(principalKinds)) + for i, pk := range principalKinds { + assert.Equal(t, environments[0].ID, pk.EnvironmentId) + actualKindIDs[i] = pk.PrincipalKind + } + + assert.ElementsMatch(t, expectedKindIDs, actualKindIDs) + }, + }, + { + name: "Success: Upsert replaces existing environment", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + err = db.Transaction(context.Background(), func(tx *database.BloodhoundDB) error { + return tx.UpsertSchemaEnvironmentWithPrincipalKinds( + context.Background(), + ext.ID, + "Tag_Tier_Zero", + "Base", + []string{"Tag_Owned"}, + ) + }) + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "Tag_Tier_Zero", + sourceKind: "Base", + principalKinds: []string{"Tag_Tier_Zero"}, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + expectedPrincipalKindNames := []string{"Tag_Tier_Zero"} + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 1, len(environments), "Should only have one environment (old one deleted)") + + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(principalKinds)) + + expectedKind, err := db.GetKindByName(context.Background(), expectedPrincipalKindNames[0]) + assert.NoError(t, err) + + assert.Equal(t, int32(expectedKind.ID), principalKinds[0].PrincipalKind) + assert.Equal(t, environments[0].ID, principalKinds[0].EnvironmentId) + }, + }, + { + name: "Success: Source kind auto-registers", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "Tag_Tier_Zero", + sourceKind: "NewSource", + principalKinds: []string{"Tag_Tier_Zero"}, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + sourceKind, err := db.GetSourceKindByName(context.Background(), "NewSource") + assert.NoError(t, err) + assert.Equal(t, graph.StringKind("NewSource"), sourceKind.Name) + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 1, len(environments)) + assert.Equal(t, int32(sourceKind.ID), environments[0].SourceKindId) + + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(principalKinds)) + }, + }, + { + name: "Error: Environment kind not found", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "NonExistent", + sourceKind: "Base", + principalKinds: []string{}, + }, + expectedError: "environment kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify transaction rolled back - no environment created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environment should exist after rollback") + }, + }, + { + name: "Error: Principal kind not found", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "Tag_Tier_Zero", + sourceKind: "Base", + principalKinds: []string{"NonExistent"}, + }, + expectedError: "principal kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify transaction rolled back - no environment created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environment should exist after rollback") + }, + }, + { + name: "Rollback: Partial failure on second principal kind", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "Tag_Tier_Zero", + sourceKind: "Base", + principalKinds: []string{"Tag_Owned", "NonExistent"}, + }, + expectedError: "principal kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify transaction rolled back - no environment created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environment should exist after rollback") + }, + }, + { + name: "Success: Multiple environments with different combinations", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + err = db.Transaction(context.Background(), func(tx *database.BloodhoundDB) error { + return tx.UpsertSchemaEnvironmentWithPrincipalKinds( + context.Background(), + ext.ID, + "Tag_Tier_Zero", + "Base", + []string{"Tag_Tier_Zero"}, + ) + }) + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environmentKind: "Tag_Owned", + sourceKind: "Base", + principalKinds: []string{"Tag_Owned"}, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 2, len(environments), "Should have two different environments") + + for _, env := range environments { + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), env.ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(principalKinds), "Each environment should have one principal kind") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testSuite := setupIntegrationTestSuite(t) + defer teardownIntegrationTestSuite(t, &testSuite) + + extensionID := tt.setupData(t, testSuite.BHDatabase) + + // Wrap the call in a transaction + err := testSuite.BHDatabase.Transaction(context.Background(), func(tx *database.BloodhoundDB) error { + return tx.UpsertSchemaEnvironmentWithPrincipalKinds( + context.Background(), + extensionID, + tt.args.environmentKind, + tt.args.sourceKind, + tt.args.principalKinds, + ) + }) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + if tt.assert != nil { + tt.assert(t, testSuite.BHDatabase) + } + } else { + require.NoError(t, err) + if tt.assert != nil { + tt.assert(t, testSuite.BHDatabase) + } + } + }) + } +} diff --git a/cmd/api/src/database/upsert_schema_extension.go b/cmd/api/src/database/upsert_schema_extension.go new file mode 100644 index 00000000000..d189a1d40f3 --- /dev/null +++ b/cmd/api/src/database/upsert_schema_extension.go @@ -0,0 +1,39 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package database + +import ( + "context" + "fmt" +) + +type EnvironmentInput struct { + EnvironmentKindName string + SourceKindName string + PrincipalKinds []string +} + +func (s *BloodhoundDB) UpsertGraphSchemaExtension(ctx context.Context, extensionID int32, environments []EnvironmentInput) error { + return s.Transaction(ctx, func(tx *BloodhoundDB) error { + for _, env := range environments { + if err := tx.UpsertSchemaEnvironmentWithPrincipalKinds(ctx, extensionID, env.EnvironmentKindName, env.SourceKindName, env.PrincipalKinds); err != nil { + return fmt.Errorf("failed to upsert environment with principal kinds: %w", err) + } + } + + return nil + }) +} diff --git a/cmd/api/src/database/upsert_schema_extension_integration_test.go b/cmd/api/src/database/upsert_schema_extension_integration_test.go new file mode 100644 index 00000000000..e4fcae02876 --- /dev/null +++ b/cmd/api/src/database/upsert_schema_extension_integration_test.go @@ -0,0 +1,433 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +//go:build integration + +package database_test + +import ( + "context" + "testing" + + "github.com/specterops/bloodhound/cmd/api/src/database" + "github.com/specterops/dawgs/graph" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBloodhoundDB_UpsertGraphSchemaExtension(t *testing.T) { + type args struct { + environments []database.EnvironmentInput + } + tests := []struct { + name string + setupData func(t *testing.T, db *database.BloodhoundDB) int32 + args args + assert func(t *testing.T, db *database.BloodhoundDB) + expectedError string + }{ + { + name: "Success: Create environment with principal kinds", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Tier_Zero", "Tag_Owned"}, + }, + }, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + expectedPrincipalKindNames := []string{"Tag_Tier_Zero", "Tag_Owned"} + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 1, len(environments)) + + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, len(expectedPrincipalKindNames), len(principalKinds)) + + expectedKindIDs := make([]int32, len(expectedPrincipalKindNames)) + for i, name := range expectedPrincipalKindNames { + kind, err := db.GetKindByName(context.Background(), name) + assert.NoError(t, err) + expectedKindIDs[i] = int32(kind.ID) + } + + actualKindIDs := make([]int32, len(principalKinds)) + for i, pk := range principalKinds { + assert.Equal(t, environments[0].ID, pk.EnvironmentId) + actualKindIDs[i] = pk.PrincipalKind + } + + assert.ElementsMatch(t, expectedKindIDs, actualKindIDs) + }, + }, + { + name: "Success: Create multiple environments", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Tier_Zero"}, + }, + { + EnvironmentKindName: "Tag_Owned", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Owned"}, + }, + }, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 2, len(environments), "Should have two environments") + + // Verify first environment + env1PrincipalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(env1PrincipalKinds)) + + // Verify second environment + env2PrincipalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[1].ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(env2PrincipalKinds)) + }, + }, + { + name: "Success: Upsert replaces existing environment", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + // Create initial environment + err = db.UpsertGraphSchemaExtension(context.Background(), ext.ID, []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Owned"}, + }, + }) + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Tier_Zero"}, + }, + }, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + expectedPrincipalKindNames := []string{"Tag_Tier_Zero"} + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 1, len(environments), "Should only have one environment (old one replaced)") + + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(principalKinds)) + + expectedKind, err := db.GetKindByName(context.Background(), expectedPrincipalKindNames[0]) + assert.NoError(t, err) + + assert.Equal(t, int32(expectedKind.ID), principalKinds[0].PrincipalKind) + assert.Equal(t, environments[0].ID, principalKinds[0].EnvironmentId) + }, + }, + { + name: "Success: Source kind auto-registers", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "NewSource", + PrincipalKinds: []string{"Tag_Tier_Zero"}, + }, + }, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + sourceKind, err := db.GetSourceKindByName(context.Background(), "NewSource") + assert.NoError(t, err) + assert.Equal(t, graph.StringKind("NewSource"), sourceKind.Name) + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 1, len(environments)) + assert.Equal(t, int32(sourceKind.ID), environments[0].SourceKindId) + + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), environments[0].ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(principalKinds)) + }, + }, + { + name: "Success: Multiple environments with different source kinds", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Tier_Zero"}, + }, + { + EnvironmentKindName: "Tag_Owned", + SourceKindName: "NewSource", + PrincipalKinds: []string{"Tag_Owned"}, + }, + }, + }, + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify NewSource was auto-registered + sourceKind, err := db.GetSourceKindByName(context.Background(), "NewSource") + assert.NoError(t, err) + assert.Equal(t, graph.StringKind("NewSource"), sourceKind.Name) + + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 2, len(environments), "Should have two environments") + + for _, env := range environments { + principalKinds, err := db.GetPrincipalKindsByEnvironmentId(context.Background(), env.ID) + assert.NoError(t, err) + assert.Equal(t, 1, len(principalKinds), "Each environment should have one principal kind") + } + }, + }, + { + name: "Error: First environment has invalid environment kind", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "NonExistent", + SourceKindName: "Base", + PrincipalKinds: []string{}, + }, + }, + }, + expectedError: "environment kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify transaction rolled back - no environment created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environment should exist after rollback") + }, + }, + { + name: "Error: First environment has invalid principal kind", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"NonExistent"}, + }, + }, + }, + expectedError: "principal kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify transaction rolled back - no environment created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environment should exist after rollback") + }, + }, + { + name: "Rollback: Second environment fails, first should rollback", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Tier_Zero"}, + }, + { + EnvironmentKindName: "NonExistent", + SourceKindName: "Base", + PrincipalKinds: []string{}, + }, + }, + }, + expectedError: "environment kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify complete transaction rollback - no environments created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environments should exist after rollback") + }, + }, + { + name: "Rollback: Second environment has invalid principal kind", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Tier_Zero"}, + }, + { + EnvironmentKindName: "Tag_Owned", + SourceKindName: "Base", + PrincipalKinds: []string{"NonExistent"}, + }, + }, + }, + expectedError: "principal kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify complete transaction rollback - no environments created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environments should exist after rollback") + }, + }, + { + name: "Rollback: Partial failure in first environment's principal kinds", + setupData: func(t *testing.T, db *database.BloodhoundDB) int32 { + t.Helper() + ext, err := db.CreateGraphSchemaExtension(context.Background(), "TestExt", "Test", "v1.0.0") + require.NoError(t, err) + + return ext.ID + }, + args: args{ + environments: []database.EnvironmentInput{ + { + EnvironmentKindName: "Tag_Tier_Zero", + SourceKindName: "Base", + PrincipalKinds: []string{"Tag_Owned", "NonExistent"}, + }, + }, + }, + expectedError: "principal kind 'NonExistent' not found", + assert: func(t *testing.T, db *database.BloodhoundDB) { + t.Helper() + + // Verify transaction rolled back - no environment created + environments, err := db.GetEnvironments(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 0, len(environments), "No environment should exist after rollback") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testSuite := setupIntegrationTestSuite(t) + defer teardownIntegrationTestSuite(t, &testSuite) + + extensionID := tt.setupData(t, testSuite.BHDatabase) + + err := testSuite.BHDatabase.UpsertGraphSchemaExtension( + context.Background(), + extensionID, + tt.args.environments, + ) + + if tt.expectedError != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + if tt.assert != nil { + tt.assert(t, testSuite.BHDatabase) + } + } else { + require.NoError(t, err) + if tt.assert != nil { + tt.assert(t, testSuite.BHDatabase) + } + } + }) + } +} diff --git a/cmd/api/src/model/kind.go b/cmd/api/src/model/kind.go new file mode 100644 index 00000000000..8f6402c6828 --- /dev/null +++ b/cmd/api/src/model/kind.go @@ -0,0 +1,21 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package model + +type Kind struct { + ID int `json:"id"` + Name string `json:"name"` +} diff --git a/cmd/api/src/services/entrypoint.go b/cmd/api/src/services/entrypoint.go index 4ab5a38745b..cb87ebfd2b5 100644 --- a/cmd/api/src/services/entrypoint.go +++ b/cmd/api/src/services/entrypoint.go @@ -39,6 +39,7 @@ import ( "github.com/specterops/bloodhound/cmd/api/src/model/appcfg" "github.com/specterops/bloodhound/cmd/api/src/queries" "github.com/specterops/bloodhound/cmd/api/src/services/dogtags" + "github.com/specterops/bloodhound/cmd/api/src/services/opengraphschema" "github.com/specterops/bloodhound/cmd/api/src/services/upload" "github.com/specterops/bloodhound/packages/go/cache" schema "github.com/specterops/bloodhound/packages/go/graphschema" @@ -118,17 +119,18 @@ func Entrypoint(ctx context.Context, cfg config.Configuration, connections boots startDelay := 0 * time.Second var ( - cl = changelog.NewChangelog(connections.Graph, connections.RDMS, changelog.DefaultOptions()) - pipeline = datapipe.NewPipeline(ctx, cfg, connections.RDMS, connections.Graph, graphQueryCache, ingestSchema, cl) - graphQuery = queries.NewGraphQuery(connections.Graph, graphQueryCache, cfg) - authorizer = auth.NewAuthorizer(connections.RDMS) - datapipeDaemon = datapipe.NewDaemon(pipeline, startDelay, time.Duration(cfg.DatapipeInterval)*time.Second, connections.RDMS) - routerInst = router.NewRouter(cfg, authorizer, fmt.Sprintf(bootstrap.ContentSecurityPolicy, "", "")) - authenticator = api.NewAuthenticator(cfg, connections.RDMS, api.NewAuthExtensions(cfg, connections.RDMS)) + cl = changelog.NewChangelog(connections.Graph, connections.RDMS, changelog.DefaultOptions()) + pipeline = datapipe.NewPipeline(ctx, cfg, connections.RDMS, connections.Graph, graphQueryCache, ingestSchema, cl) + graphQuery = queries.NewGraphQuery(connections.Graph, graphQueryCache, cfg) + authorizer = auth.NewAuthorizer(connections.RDMS) + datapipeDaemon = datapipe.NewDaemon(pipeline, startDelay, time.Duration(cfg.DatapipeInterval)*time.Second, connections.RDMS) + routerInst = router.NewRouter(cfg, authorizer, fmt.Sprintf(bootstrap.ContentSecurityPolicy, "", "")) + authenticator = api.NewAuthenticator(cfg, connections.RDMS, api.NewAuthExtensions(cfg, connections.RDMS)) + openGraphSchemaService = opengraphschema.NewOpenGraphSchemaService(connections.RDMS) ) registration.RegisterFossGlobalMiddleware(&routerInst, cfg, auth.NewIdentityResolver(), authenticator) - registration.RegisterFossRoutes(&routerInst, cfg, connections.RDMS, connections.Graph, graphQuery, apiCache, collectorManifests, authenticator, authorizer, ingestSchema, dogtagsService) + registration.RegisterFossRoutes(&routerInst, cfg, connections.RDMS, connections.Graph, graphQuery, apiCache, collectorManifests, authenticator, authorizer, ingestSchema, dogtagsService, openGraphSchemaService) // Set neo4j batch and flush sizes neo4jParameters := appcfg.GetNeo4jParameters(ctx, connections.RDMS) diff --git a/cmd/api/src/services/opengraphschema/extension.go b/cmd/api/src/services/opengraphschema/extension.go new file mode 100644 index 00000000000..a16df48021c --- /dev/null +++ b/cmd/api/src/services/opengraphschema/extension.go @@ -0,0 +1,46 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package opengraphschema + +import ( + "context" + "fmt" + + v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2" + "github.com/specterops/bloodhound/cmd/api/src/database" +) + +func (s *OpenGraphSchemaService) UpsertGraphSchemaExtension(ctx context.Context, req v2.GraphSchemaExtension) error { + var ( + environments = make([]database.EnvironmentInput, len(req.Environments)) + ) + + for i, environment := range req.Environments { + environments[i] = database.EnvironmentInput{ + EnvironmentKindName: environment.EnvironmentKind, + SourceKindName: environment.SourceKind, + PrincipalKinds: environment.PrincipalKinds, + } + } + + // TODO: Temporary hardcoded value but needs to be updated to pass in the extension ID + err := s.openGraphSchemaRepository.UpsertGraphSchemaExtension(ctx, 1, environments) + if err != nil { + return fmt.Errorf("error upserting graph extension: %w", err) + } + + return nil +} diff --git a/cmd/api/src/services/opengraphschema/extension_test.go b/cmd/api/src/services/opengraphschema/extension_test.go new file mode 100644 index 00000000000..0f81f8b881e --- /dev/null +++ b/cmd/api/src/services/opengraphschema/extension_test.go @@ -0,0 +1,164 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package opengraphschema_test + +import ( + "context" + "errors" + "testing" + + v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2" + "github.com/specterops/bloodhound/cmd/api/src/database" + "github.com/specterops/bloodhound/cmd/api/src/services/opengraphschema" + schemamocks "github.com/specterops/bloodhound/cmd/api/src/services/opengraphschema/mocks" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +func TestOpenGraphSchemaService_UpsertGraphSchemaExtension(t *testing.T) { + type mocks struct { + mockOpenGraphSchema *schemamocks.MockOpenGraphSchemaRepository + } + type args struct { + environments []v2.Environment + } + tests := []struct { + name string + setupMocks func(t *testing.T, m *mocks) + args args + expected error + }{ + { + name: "Error: openGraphSchemaRepository.UpsertGraphSchemaExtension error", + args: args{ + environments: []v2.Environment{ + { + EnvironmentKind: "Domain", + SourceKind: "Base", + PrincipalKinds: []string{"User"}, + }, + }, + }, + setupMocks: func(t *testing.T, m *mocks) { + t.Helper() + expectedEnvs := []database.EnvironmentInput{ + { + EnvironmentKindName: "Domain", + SourceKindName: "Base", + PrincipalKinds: []string{"User"}, + }, + } + m.mockOpenGraphSchema.EXPECT().UpsertGraphSchemaExtension( + gomock.Any(), + int32(1), + expectedEnvs, + ).Return(errors.New("error")) + }, + expected: errors.New("error upserting graph extension: error"), + }, + { + name: "Success: single environment", + args: args{ + environments: []v2.Environment{ + { + EnvironmentKind: "Domain", + SourceKind: "Base", + PrincipalKinds: []string{"User", "Computer"}, + }, + }, + }, + setupMocks: func(t *testing.T, m *mocks) { + t.Helper() + expectedEnvs := []database.EnvironmentInput{ + { + EnvironmentKindName: "Domain", + SourceKindName: "Base", + PrincipalKinds: []string{"User", "Computer"}, + }, + } + m.mockOpenGraphSchema.EXPECT().UpsertGraphSchemaExtension( + gomock.Any(), + int32(1), + expectedEnvs, + ).Return(nil) + }, + expected: nil, + }, + { + name: "Success: multiple environments", + args: args{ + environments: []v2.Environment{ + { + EnvironmentKind: "Domain", + SourceKind: "Base", + PrincipalKinds: []string{"User"}, + }, + { + EnvironmentKind: "AzureAD", + SourceKind: "AzureHound", + PrincipalKinds: []string{"User", "Group"}, + }, + }, + }, + setupMocks: func(t *testing.T, m *mocks) { + t.Helper() + expectedEnvs := []database.EnvironmentInput{ + { + EnvironmentKindName: "Domain", + SourceKindName: "Base", + PrincipalKinds: []string{"User"}, + }, + { + EnvironmentKindName: "AzureAD", + SourceKindName: "AzureHound", + PrincipalKinds: []string{"User", "Group"}, + }, + } + m.mockOpenGraphSchema.EXPECT().UpsertGraphSchemaExtension( + gomock.Any(), + int32(1), + expectedEnvs, + ).Return(nil) + }, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + m := &mocks{ + mockOpenGraphSchema: schemamocks.NewMockOpenGraphSchemaRepository(ctrl), + } + + tt.setupMocks(t, m) + + service := opengraphschema.NewOpenGraphSchemaService(m.mockOpenGraphSchema) + + err := service.UpsertGraphSchemaExtension(context.Background(), v2.GraphSchemaExtension{ + Environments: tt.args.environments, + }) + + if tt.expected != nil { + assert.EqualError(t, err, tt.expected.Error()) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/api/src/services/opengraphschema/mocks/opengraphschema.go b/cmd/api/src/services/opengraphschema/mocks/opengraphschema.go new file mode 100644 index 00000000000..f1e1539a2ab --- /dev/null +++ b/cmd/api/src/services/opengraphschema/mocks/opengraphschema.go @@ -0,0 +1,72 @@ +// Copyright 2026 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/specterops/bloodhound/cmd/api/src/services/opengraphschema (interfaces: OpenGraphSchemaRepository) +// +// Generated by this command: +// +// mockgen -copyright_file ../../../../../LICENSE.header -destination=./mocks/opengraphschema.go -package=mocks . OpenGraphSchemaRepository +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + database "github.com/specterops/bloodhound/cmd/api/src/database" + gomock "go.uber.org/mock/gomock" +) + +// MockOpenGraphSchemaRepository is a mock of OpenGraphSchemaRepository interface. +type MockOpenGraphSchemaRepository struct { + ctrl *gomock.Controller + recorder *MockOpenGraphSchemaRepositoryMockRecorder + isgomock struct{} +} + +// MockOpenGraphSchemaRepositoryMockRecorder is the mock recorder for MockOpenGraphSchemaRepository. +type MockOpenGraphSchemaRepositoryMockRecorder struct { + mock *MockOpenGraphSchemaRepository +} + +// NewMockOpenGraphSchemaRepository creates a new mock instance. +func NewMockOpenGraphSchemaRepository(ctrl *gomock.Controller) *MockOpenGraphSchemaRepository { + mock := &MockOpenGraphSchemaRepository{ctrl: ctrl} + mock.recorder = &MockOpenGraphSchemaRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOpenGraphSchemaRepository) EXPECT() *MockOpenGraphSchemaRepositoryMockRecorder { + return m.recorder +} + +// UpsertGraphSchemaExtension mocks base method. +func (m *MockOpenGraphSchemaRepository) UpsertGraphSchemaExtension(ctx context.Context, extensionID int32, environments []database.EnvironmentInput) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpsertGraphSchemaExtension", ctx, extensionID, environments) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpsertGraphSchemaExtension indicates an expected call of UpsertGraphSchemaExtension. +func (mr *MockOpenGraphSchemaRepositoryMockRecorder) UpsertGraphSchemaExtension(ctx, extensionID, environments any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertGraphSchemaExtension", reflect.TypeOf((*MockOpenGraphSchemaRepository)(nil).UpsertGraphSchemaExtension), ctx, extensionID, environments) +} diff --git a/cmd/api/src/services/opengraphschema/opengraphschema.go b/cmd/api/src/services/opengraphschema/opengraphschema.go new file mode 100644 index 00000000000..74322b76bf2 --- /dev/null +++ b/cmd/api/src/services/opengraphschema/opengraphschema.go @@ -0,0 +1,39 @@ +// Copyright 2025 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +package opengraphschema + +//go:generate go run go.uber.org/mock/mockgen -copyright_file ../../../../../LICENSE.header -destination=./mocks/opengraphschema.go -package=mocks . OpenGraphSchemaRepository + +import ( + "context" + + "github.com/specterops/bloodhound/cmd/api/src/database" +) + +// OpenGraphSchemaRepository - +type OpenGraphSchemaRepository interface { + UpsertGraphSchemaExtension(ctx context.Context, extensionID int32, environments []database.EnvironmentInput) error +} + +type OpenGraphSchemaService struct { + openGraphSchemaRepository OpenGraphSchemaRepository +} + +func NewOpenGraphSchemaService(openGraphSchemaRepository OpenGraphSchemaRepository) *OpenGraphSchemaService { + return &OpenGraphSchemaService{ + openGraphSchemaRepository: openGraphSchemaRepository, + } +}