Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ import (
"time"

"github.com/gofrs/uuid"
"gorm.io/driver/postgres"
"gorm.io/gorm"

"github.com/specterops/bloodhound/cmd/api/src/auth"
"github.com/specterops/bloodhound/cmd/api/src/database/migration"
"github.com/specterops/bloodhound/cmd/api/src/model"
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
"github.com/specterops/bloodhound/cmd/api/src/services/agi"
"github.com/specterops/bloodhound/cmd/api/src/services/dataquality"
"github.com/specterops/bloodhound/cmd/api/src/services/upload"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)

var (
Expand All @@ -56,6 +57,7 @@ var (
ErrDuplicateSchemaNodeKindName = errors.New("duplicate schema node kind name")
ErrDuplicateGraphSchemaExtensionPropertyName = errors.New("duplicate graph schema extension property name")
ErrDuplicateSchemaEdgeKindName = errors.New("duplicate schema edge kind name")
ErrDuplicateSchemaEnvironment = errors.New("duplicate schema environment")
)

func IsUnexpectedDatabaseError(err error) bool {
Expand Down Expand Up @@ -242,6 +244,11 @@ func (s *BloodhoundDB) RawDelete(value any) error {
return CheckError(s.db.Delete(value))
}

// RawFirst executes a raw SQL query and scans the first result into dest.
func (s *BloodhoundDB) RawFirst(ctx context.Context, sql string, dest any, values ...any) error {
return CheckError(s.db.WithContext(ctx).Raw(sql, values...).Scan(dest))
}

func (s *BloodhoundDB) Wipe(ctx context.Context) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var tables []string
Expand Down
48 changes: 45 additions & 3 deletions cmd/api/src/database/graphschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import (
"fmt"
"strings"

"github.com/specterops/bloodhound/cmd/api/src/model"
"gorm.io/gorm"

"github.com/specterops/bloodhound/cmd/api/src/model"
)

type OpenGraphSchema interface {
Expand All @@ -42,6 +43,10 @@ type OpenGraphSchema interface {
GetSchemaEdgeKindById(ctx context.Context, schemaEdgeKindId int32) (model.SchemaEdgeKind, error)
UpdateSchemaEdgeKind(ctx context.Context, schemaEdgeKind model.SchemaEdgeKind) (model.SchemaEdgeKind, error)
DeleteSchemaEdgeKind(ctx context.Context, schemaEdgeKindId int32) error

CreateSchemaEnvironment(ctx context.Context, schemaExtensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error)
GetSchemaEnvironmentById(ctx context.Context, schemaEnvironmentId int32) (model.SchemaEnvironment, error)
DeleteSchemaEnvironment(ctx context.Context, schemaEnvironmentId int32) error
}

// CreateGraphSchemaExtension creates a new row in the extensions table. A GraphSchemaExtension struct is returned, populated with the value as it stands in the database.
Expand Down Expand Up @@ -123,7 +128,7 @@ func (s *BloodhoundDB) GetSchemaNodeKindById(ctx context.Context, schemaNodeKind
// UpdateSchemaNodeKind - updates a row in the schema_node_kinds table based on the provided id. It will return an error if the target schema node kind does not exist or if any of the updates violate the schema constraints.
func (s *BloodhoundDB) UpdateSchemaNodeKind(ctx context.Context, schemaNodeKind model.SchemaNodeKind) (model.SchemaNodeKind, error) {
if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(`
UPDATE %s
UPDATE %s
SET name = ?, schema_extension_id = ?, display_name = ?, description = ?, is_display_kind = ?, icon = ?, icon_color = ?, updated_at = NOW()
WHERE id = ?
RETURNING id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at`,
Expand Down Expand Up @@ -186,7 +191,7 @@ func (s *BloodhoundDB) GetGraphSchemaPropertyById(ctx context.Context, extension

func (s *BloodhoundDB) UpdateGraphSchemaProperty(ctx context.Context, property model.GraphSchemaProperty) (model.GraphSchemaProperty, error) {
if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(`
UPDATE %s SET name = ?, display_name = ?, data_type = ?, description = ?, updated_at = NOW() WHERE id = ?
UPDATE %s SET name = ?, display_name = ?, data_type = ?, description = ?, updated_at = NOW() WHERE id = ?
RETURNING id, schema_extension_id, name, display_name, data_type, description, created_at, updated_at, deleted_at`,
property.TableName()),
property.Name, property.DisplayName, property.DataType, property.Description, property.ID).Scan(&property); result.Error != nil {
Expand Down Expand Up @@ -269,3 +274,40 @@ func (s *BloodhoundDB) DeleteSchemaEdgeKind(ctx context.Context, schemaEdgeKindI
}
return nil
}

// CreateSchemaEnvironment - creates a new schema_environment.
func (s *BloodhoundDB) CreateSchemaEnvironment(ctx context.Context, schemaExtensionId int32, environmentKindId int32, sourceKindId int32) (model.SchemaEnvironment, error) {
var schemaEnvironment model.SchemaEnvironment

if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(`
INSERT INTO %s (schema_extension_id, environment_kind_id, source_kind_id)
VALUES (?, ?, ?)
RETURNING id, schema_extension_id, environment_kind_id, source_kind_id`,
schemaEnvironment.TableName()),
schemaExtensionId, environmentKindId, sourceKindId).Scan(&schemaEnvironment); result.Error != nil {
if strings.Contains(result.Error.Error(), "duplicate key value violates unique constraint") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you pull main, there's a const you can use defined for this error

const DuplicateKeyValueErrorString = "duplicate key value violates unique constraint"

Everything else looks good.

return model.SchemaEnvironment{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaEnvironment, result.Error)
}
return model.SchemaEnvironment{}, CheckError(result)
}
return schemaEnvironment, nil
}

// GetSchemaEnvironmentById - retrieves a schema_environment by id.
func (s *BloodhoundDB) GetSchemaEnvironmentById(ctx context.Context, schemaEnvironmentId int32) (model.SchemaEnvironment, error) {
var schemaEnvironment model.SchemaEnvironment
return schemaEnvironment, CheckError(s.db.WithContext(ctx).Raw(fmt.Sprintf(`
SELECT id, schema_extension_id, environment_kind_id, source_kind_id
FROM %s WHERE id = ?`, schemaEnvironment.TableName()), schemaEnvironmentId).First(&schemaEnvironment))
}

// DeleteSchemaEnvironment - deletes a schema_environment by id.
func (s *BloodhoundDB) DeleteSchemaEnvironment(ctx context.Context, schemaEnvironmentId int32) error {
var schemaEnvironment model.SchemaEnvironment
if result := s.db.WithContext(ctx).Exec(fmt.Sprintf(`DELETE FROM %s WHERE id = ?`, schemaEnvironment.TableName()), schemaEnvironmentId); result.Error != nil {
return CheckError(result)
} else if result.RowsAffected == 0 {
return ErrNotFound
}
return nil
}
86 changes: 86 additions & 0 deletions cmd/api/src/database/graphschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,89 @@ func compareSchemaEdgeKind(t *testing.T, got, want model.SchemaEdgeKind) {
require.Equalf(t, want.IsTraversable, got.IsTraversable, "CreateSchemaEdgeKind - IsTraversable - got %v, want %t", got.IsTraversable, want.IsTraversable)
require.Equalf(t, want.SchemaExtensionId, got.SchemaExtensionId, "CreateSchemaEdgeKind - SchemaExtensionId - got %d, want %d", got.SchemaExtensionId, want.SchemaExtensionId)
}

func TestDatabase_SchemaEnvironment_CRUD(t *testing.T) {
t.Parallel()
testSuite := setupIntegrationTestSuite(t)
defer teardownIntegrationTestSuite(t, &testSuite)

// Create prerequisite extension
extension, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "test_extension_schema_environments", "test_extension", "1.0.0")
require.NoError(t, err)

// Insert test kinds into the kind table for foreign key references
var environmentKindId1, environmentKindId2, sourceKindId1, sourceKindId2 int32
err = testSuite.BHDatabase.RawFirst(testSuite.Context, "INSERT INTO kind (name) VALUES ('TestSchemaEnvEnvironmentKind1') RETURNING id", &environmentKindId1)
require.NoError(t, err)
err = testSuite.BHDatabase.RawFirst(testSuite.Context, "INSERT INTO kind (name) VALUES ('TestSchemaEnvEnvironmentKind2') RETURNING id", &environmentKindId2)
require.NoError(t, err)
err = testSuite.BHDatabase.RawFirst(testSuite.Context, "INSERT INTO kind (name) VALUES ('TestSchemaEnvSourceKind1') RETURNING id", &sourceKindId1)
require.NoError(t, err)
err = testSuite.BHDatabase.RawFirst(testSuite.Context, "INSERT INTO kind (name) VALUES ('TestSchemaEnvSourceKind2') RETURNING id", &sourceKindId2)
require.NoError(t, err)

var (
gotEnv1 = model.SchemaEnvironment{}
gotEnv2 = model.SchemaEnvironment{}
)

// Expected success - create first schema environment
t.Run("success - create schema environment #1", func(t *testing.T) {
var createErr error
gotEnv1, createErr = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, extension.ID, environmentKindId1, sourceKindId1)
require.NoError(t, createErr)
require.Equal(t, extension.ID, gotEnv1.SchemaExtensionId)
require.Equal(t, environmentKindId1, gotEnv1.EnvironmentKindId)
require.Equal(t, sourceKindId1, gotEnv1.SourceKindId)
})

// Expected success - create second schema environment with different kind combination
t.Run("success - create schema environment #2", func(t *testing.T) {
var createErr error
gotEnv2, createErr = testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, extension.ID, environmentKindId2, sourceKindId2)
require.NoError(t, createErr)
require.Equal(t, extension.ID, gotEnv2.SchemaExtensionId)
require.Equal(t, environmentKindId2, gotEnv2.EnvironmentKindId)
require.Equal(t, sourceKindId2, gotEnv2.SourceKindId)
})

// Expected success - get schema environment by id
t.Run("success - get schema environment #1", func(t *testing.T) {
got, getErr := testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, gotEnv1.ID)
require.NoError(t, getErr)
require.Equal(t, gotEnv1.ID, got.ID)
require.Equal(t, gotEnv1.SchemaExtensionId, got.SchemaExtensionId)
require.Equal(t, gotEnv1.EnvironmentKindId, got.EnvironmentKindId)
require.Equal(t, gotEnv1.SourceKindId, got.SourceKindId)
})

// Expected fail - duplicate (environment_kind_id, source_kind_id) combination
t.Run("fail - create schema environment with duplicate kind combination", func(t *testing.T) {
_, dupErr := testSuite.BHDatabase.CreateSchemaEnvironment(testSuite.Context, extension.ID, environmentKindId1, sourceKindId1)
require.ErrorIs(t, dupErr, database.ErrDuplicateSchemaEnvironment)
})

// Expected success - delete schema environment
t.Run("success - delete schema environment #1", func(t *testing.T) {
delErr := testSuite.BHDatabase.DeleteSchemaEnvironment(testSuite.Context, gotEnv1.ID)
require.NoError(t, delErr)
})

// Expected fail - get deleted schema environment
t.Run("fail - get schema environment that does not exist", func(t *testing.T) {
_, getErr := testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, gotEnv1.ID)
require.ErrorIs(t, getErr, database.ErrNotFound)
})

// Expected fail - delete non-existent schema environment
t.Run("fail - delete schema environment that does not exist", func(t *testing.T) {
delErr := testSuite.BHDatabase.DeleteSchemaEnvironment(testSuite.Context, gotEnv1.ID)
require.ErrorIs(t, delErr, database.ErrNotFound)
})

// Expected fail - get schema environment with invalid id
t.Run("fail - get schema environment with invalid id", func(t *testing.T) {
_, getErr := testSuite.BHDatabase.GetSchemaEnvironmentById(testSuite.Context, 999999)
require.ErrorIs(t, getErr, database.ErrNotFound)
})
}
69 changes: 55 additions & 14 deletions cmd/api/src/database/migration/migrations/v8.5.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,6 @@ CREATE TABLE IF NOT EXISTS schema_node_kinds (

CREATE INDEX idx_graph_schema_node_kinds_extensions_id ON schema_node_kinds (schema_extension_id);

-- OpenGraph graph schema - extensions (collectors)
CREATE TABLE IF NOT EXISTS schema_extensions (
id SERIAL NOT NULL,
name TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
version TEXT NOT NULL,
is_builtin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp,
deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL,
PRIMARY KEY (id)
);

-- OpenGraph schema properties
CREATE TABLE IF NOT EXISTS schema_properties (
id SERIAL NOT NULL,
Expand All @@ -85,6 +72,7 @@ CREATE TABLE IF NOT EXISTS schema_properties (
);

CREATE INDEX idx_schema_properties_schema_extensions_id on schema_properties (schema_extension_id);

-- OpenGraph schema_edge_kinds - store edge kinds for open graph extensions
CREATE TABLE IF NOT EXISTS schema_edge_kinds (
id SERIAL NOT NULL,
Expand All @@ -98,4 +86,57 @@ CREATE TABLE IF NOT EXISTS schema_edge_kinds (
PRIMARY KEY (id)
);

CREATE INDEX idx_schema_edge_kinds_extensions_id ON schema_edge_kinds (schema_extension_id);
CREATE INDEX idx_schema_edge_kinds_extensions_id ON schema_edge_kinds (schema_extension_id);

-- OpenGraph schema_environments - stores environment mappings.
CREATE TABLE IF NOT EXISTS schema_environments (
id SERIAL,
schema_extension_id INTEGER NOT NULL REFERENCES schema_extensions(id) ON DELETE CASCADE,
environment_kind_id INTEGER NOT NULL REFERENCES kind(id),
source_kind_id INTEGER NOT NULL REFERENCES kind(id),
PRIMARY KEY (id),
UNIQUE(environment_kind_id,source_kind_id)
);

CREATE INDEX idx_schema_environments_extension_id ON schema_environments (schema_extension_id);

-- OpenGraph schema_relationship_findings - Individual findings. ie T0WriteOwner, T0ADCSESC1, T0DCSync
CREATE TABLE IF NOT EXISTS schema_relationship_findings (
id SERIAL,
schema_extension_id INTEGER NOT NULL REFERENCES schema_extensions(id) ON DELETE CASCADE,
relationship_kind_id INTEGER NOT NULL REFERENCES kind(id),
environment_id INTEGER NOT NULL REFERENCES schema_environments(id) ON DELETE CASCADE,
name TEXT NOT NULL,
display_name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp,
PRIMARY KEY(id),
UNIQUE(name)
);

CREATE INDEX idx_schema_relationship_findings_extension_id ON schema_relationship_findings (schema_extension_id);
CREATE INDEX idx_schema_relationship_findings_environment_id ON schema_relationship_findings(environment_id);

-- OpenGraph schema_remediations - Remediation content table with FK to findings
CREATE TABLE IF NOT EXISTS schema_remediations (
id SERIAL,
finding_id INTEGER NOT NULL REFERENCES schema_relationship_findings(id) ON DELETE CASCADE,
short_description TEXT,
long_description TEXT,
short_remediation TEXT,
long_remediation TEXT,
PRIMARY KEY(id),
UNIQUE(finding_id)
);

CREATE INDEX idx_schema_remediations_finding_id ON schema_remediations(finding_id);

-- OpenGraph schema_environments_principal_kinds - Environment to principal mappings
CREATE TABLE IF NOT EXISTS schema_environments_principal_kinds (
id SERIAL,
environment_id INTEGER NOT NULL REFERENCES schema_environments(id) ON DELETE CASCADE,
principal_kind INTEGER NOT NULL REFERENCES kind(id),
PRIMARY KEY(id),
UNIQUE(principal_kind)
);

CREATE INDEX idx_schema_environments_principal_kinds_environment_id ON schema_environments_principal_kinds (environment_id);
44 changes: 44 additions & 0 deletions cmd/api/src/database/mocks/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions cmd/api/src/model/graphschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,15 @@ type SchemaEdgeKind struct {
func (SchemaEdgeKind) TableName() string {
return "schema_edge_kinds"
}

// SchemaEnvironment - represents an environment mapping for an extension
type SchemaEnvironment struct {
ID int32 `json:"id" gorm:"primaryKey"`
SchemaExtensionId int32 `json:"schema_extension_id"`
EnvironmentKindId int32 `json:"environment_kind_id"`
SourceKindId int32 `json:"source_kind_id"`
}

func (SchemaEnvironment) TableName() string {
return "schema_environments"
}