From 3614df661f1fdca0896c596800ce77b84db4b0fd Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Wed, 17 Dec 2025 16:47:48 -0500 Subject: [PATCH 01/27] BED-6721: generate sql --- .../src/database/migration/extensions/ad.sql | 132 ++++++++ .../src/database/migration/extensions/az.sql | 99 ++++++ packages/go/schemagen/generator/cue.go | 5 +- packages/go/schemagen/generator/sql.go | 293 ++++++++++++++++++ packages/go/schemagen/main.go | 26 +- 5 files changed, 550 insertions(+), 5 deletions(-) create mode 100644 cmd/api/src/database/migration/extensions/ad.sql create mode 100644 cmd/api/src/database/migration/extensions/az.sql create mode 100644 packages/go/schemagen/generator/sql.go diff --git a/cmd/api/src/database/migration/extensions/ad.sql b/cmd/api/src/database/migration/extensions/ad.sql new file mode 100644 index 00000000000..0903e74e842 --- /dev/null +++ b/cmd/api/src/database/migration/extensions/ad.sql @@ -0,0 +1,132 @@ +-- 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 +-- Code generated by Cuelang code gen. DO NOT EDIT! +-- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ +DELETE FROM schema_extensions WHERE name = 'AD'; + +INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true); + +WITH schema_id AS ( + SELECT id FROM schema_extensions WHERE name = 'AD' +) +INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES + (schema_id, 'Entity', '', '', false, '', ''), + (schema_id, 'User', '', '', true, 'fa-user', '#17E625'), + (schema_id, 'Computer', '', '', true, 'fa-desktop', '#E67873'), + (schema_id, 'Group', '', '', true, 'fa-users', '#DBE617'), + (schema_id, 'GPO', '', '', true, 'fa-list', '#998EFD'), + (schema_id, 'OU', '', '', true, 'fa-sitemap', '#FFAA00'), + (schema_id, 'Container', '', '', true, 'fa-box', '#F79A78'), + (schema_id, 'Domain', '', '', true, 'fa-globe', '#17E6B9'), + (schema_id, 'LocalGroup', '', '', false, '', ''), + (schema_id, 'LocalUser', '', '', false, '', ''), + (schema_id, 'AIACA', '', '', true, 'fa-arrows-left-right-to-line', '#9769F0'), + (schema_id, 'RootCA', '', '', true, 'fa-landmark', '#6968E8'), + (schema_id, 'EnterpriseCA', '', '', true, 'fa-building', '#4696E9'), + (schema_id, 'NTAuthStore', '', '', true, 'fa-store', '#D575F5'), + (schema_id, 'CertTemplate', '', '', true, 'fa-id-card', '#B153F3'), + (schema_id, 'IssuancePolicy', '', '', true, 'fa-clipboard-check', '#99B2DD'); + +WITH schema_id AS ( + SELECT id FROM schema_extensions WHERE name = 'AD' +) +INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES + (schema_id, 'Owns', 'Owns', '', true), + (schema_id, 'GenericAll', 'GenericAll', '', true), + (schema_id, 'GenericWrite', 'GenericWrite', '', true), + (schema_id, 'WriteOwner', 'WriteOwner', '', true), + (schema_id, 'WriteDACL', 'WriteDACL', '', true), + (schema_id, 'MemberOf', 'MemberOf', '', true), + (schema_id, 'ForceChangePassword', 'ForceChangePassword', '', true), + (schema_id, 'AllExtendedRights', 'AllExtendedRights', '', true), + (schema_id, 'AddMember', 'AddMember', '', true), + (schema_id, 'HasSession', 'HasSession', '', true), + (schema_id, 'Contains', 'Contains', '', true), + (schema_id, 'GPLink', 'GPLink', '', true), + (schema_id, 'AllowedToDelegate', 'AllowedToDelegate', '', true), + (schema_id, 'CoerceToTGT', 'CoerceToTGT', '', true), + (schema_id, 'GetChanges', 'GetChanges', '', false), + (schema_id, 'GetChangesAll', 'GetChangesAll', '', false), + (schema_id, 'GetChangesInFilteredSet', 'GetChangesInFilteredSet', '', false), + (schema_id, 'CrossForestTrust', 'CrossForestTrust', '', false), + (schema_id, 'SameForestTrust', 'SameForestTrust', '', true), + (schema_id, 'SpoofSIDHistory', 'SpoofSIDHistory', '', true), + (schema_id, 'AbuseTGTDelegation', 'AbuseTGTDelegation', '', true), + (schema_id, 'AllowedToAct', 'AllowedToAct', '', true), + (schema_id, 'AdminTo', 'AdminTo', '', true), + (schema_id, 'CanPSRemote', 'CanPSRemote', '', true), + (schema_id, 'CanRDP', 'CanRDP', '', true), + (schema_id, 'ExecuteDCOM', 'ExecuteDCOM', '', true), + (schema_id, 'HasSIDHistory', 'HasSIDHistory', '', true), + (schema_id, 'AddSelf', 'AddSelf', '', true), + (schema_id, 'DCSync', 'DCSync', '', true), + (schema_id, 'ReadLAPSPassword', 'ReadLAPSPassword', '', true), + (schema_id, 'ReadGMSAPassword', 'ReadGMSAPassword', '', true), + (schema_id, 'DumpSMSAPassword', 'DumpSMSAPassword', '', true), + (schema_id, 'SQLAdmin', 'SQLAdmin', '', true), + (schema_id, 'AddAllowedToAct', 'AddAllowedToAct', '', true), + (schema_id, 'WriteSPN', 'WriteSPN', '', true), + (schema_id, 'AddKeyCredentialLink', 'AddKeyCredentialLink', '', true), + (schema_id, 'LocalToComputer', 'LocalToComputer', '', false), + (schema_id, 'MemberOfLocalGroup', 'MemberOfLocalGroup', '', false), + (schema_id, 'RemoteInteractiveLogonRight', 'RemoteInteractiveLogonRight', '', false), + (schema_id, 'SyncLAPSPassword', 'SyncLAPSPassword', '', true), + (schema_id, 'WriteAccountRestrictions', 'WriteAccountRestrictions', '', true), + (schema_id, 'WriteGPLink', 'WriteGPLink', '', true), + (schema_id, 'RootCAFor', 'RootCAFor', '', false), + (schema_id, 'DCFor', 'DCFor', '', true), + (schema_id, 'PublishedTo', 'PublishedTo', '', false), + (schema_id, 'ManageCertificates', 'ManageCertificates', '', true), + (schema_id, 'ManageCA', 'ManageCA', '', true), + (schema_id, 'DelegatedEnrollmentAgent', 'DelegatedEnrollmentAgent', '', false), + (schema_id, 'Enroll', 'Enroll', '', false), + (schema_id, 'HostsCAService', 'HostsCAService', '', false), + (schema_id, 'WritePKIEnrollmentFlag', 'WritePKIEnrollmentFlag', '', false), + (schema_id, 'WritePKINameFlag', 'WritePKINameFlag', '', false), + (schema_id, 'NTAuthStoreFor', 'NTAuthStoreFor', '', false), + (schema_id, 'TrustedForNTAuth', 'TrustedForNTAuth', '', false), + (schema_id, 'EnterpriseCAFor', 'EnterpriseCAFor', '', false), + (schema_id, 'IssuedSignedBy', 'IssuedSignedBy', '', false), + (schema_id, 'GoldenCert', 'GoldenCert', '', true), + (schema_id, 'EnrollOnBehalfOf', 'EnrollOnBehalfOf', '', false), + (schema_id, 'OIDGroupLink', 'OIDGroupLink', '', false), + (schema_id, 'ExtendedByPolicy', 'ExtendedByPolicy', '', false), + (schema_id, 'ADCSESC1', 'ADCSESC1', '', true), + (schema_id, 'ADCSESC3', 'ADCSESC3', '', true), + (schema_id, 'ADCSESC4', 'ADCSESC4', '', true), + (schema_id, 'ADCSESC6a', 'ADCSESC6a', '', true), + (schema_id, 'ADCSESC6b', 'ADCSESC6b', '', true), + (schema_id, 'ADCSESC9a', 'ADCSESC9a', '', true), + (schema_id, 'ADCSESC9b', 'ADCSESC9b', '', true), + (schema_id, 'ADCSESC10a', 'ADCSESC10a', '', true), + (schema_id, 'ADCSESC10b', 'ADCSESC10b', '', true), + (schema_id, 'ADCSESC13', 'ADCSESC13', '', true), + (schema_id, 'SyncedToEntraUser', 'SyncedToEntraUser', '', true), + (schema_id, 'CoerceAndRelayNTLMToSMB', 'CoerceAndRelayNTLMToSMB', '', true), + (schema_id, 'CoerceAndRelayNTLMToADCS', 'CoerceAndRelayNTLMToADCS', '', true), + (schema_id, 'WriteOwnerLimitedRights', 'WriteOwnerLimitedRights', '', true), + (schema_id, 'WriteOwnerRaw', 'WriteOwnerRaw', '', false), + (schema_id, 'OwnsLimitedRights', 'OwnsLimitedRights', '', true), + (schema_id, 'OwnsRaw', 'OwnsRaw', '', false), + (schema_id, 'ClaimSpecialIdentity', 'ClaimSpecialIdentity', '', true), + (schema_id, 'CoerceAndRelayNTLMToLDAP', 'CoerceAndRelayNTLMToLDAP', '', true), + (schema_id, 'CoerceAndRelayNTLMToLDAPS', 'CoerceAndRelayNTLMToLDAPS', '', true), + (schema_id, 'ContainsIdentity', 'ContainsIdentity', '', true), + (schema_id, 'PropagatesACEsTo', 'PropagatesACEsTo', '', true), + (schema_id, 'GPOAppliesTo', 'GPOAppliesTo', '', true), + (schema_id, 'CanApplyGPO', 'CanApplyGPO', '', true), + (schema_id, 'HasTrustKeys', 'HasTrustKeys', '', true), + (schema_id, 'ProtectAdminGroups', 'ProtectAdminGroups', '', false); diff --git a/cmd/api/src/database/migration/extensions/az.sql b/cmd/api/src/database/migration/extensions/az.sql new file mode 100644 index 00000000000..aac87492fad --- /dev/null +++ b/cmd/api/src/database/migration/extensions/az.sql @@ -0,0 +1,99 @@ +-- 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 +-- Code generated by Cuelang code gen. DO NOT EDIT! +-- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ +DELETE FROM schema_extensions WHERE name = 'AZ'; + +INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true); + +WITH schema_id AS ( + SELECT id FROM schema_extensions WHERE name = 'AZ' +) +INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES + (schema_id, 'Entity', '', '', false, '', ''), + (schema_id, 'VMScaleSet', '', '', false, '', ''), + (schema_id, 'App', '', '', false, '', ''), + (schema_id, 'Role', '', '', false, '', ''), + (schema_id, 'Device', '', '', false, '', ''), + (schema_id, 'FunctionApp', '', '', false, '', ''), + (schema_id, 'Group', '', '', true, 'fa-users', '#DBE617'), + (schema_id, 'KeyVault', '', '', false, '', ''), + (schema_id, 'ManagementGroup', '', '', false, '', ''), + (schema_id, 'ResourceGroup', '', '', false, '', ''), + (schema_id, 'ServicePrincipal', '', '', false, '', ''), + (schema_id, 'Subscription', '', '', false, '', ''), + (schema_id, 'Tenant', '', '', false, '', ''), + (schema_id, 'User', '', '', true, 'fa-user', '#17E625'), + (schema_id, 'VM', '', '', false, '', ''), + (schema_id, 'ManagedCluster', '', '', false, '', ''), + (schema_id, 'ContainerRegistry', '', '', false, '', ''), + (schema_id, 'WebApp', '', '', false, '', ''), + (schema_id, 'LogicApp', '', '', false, '', ''), + (schema_id, 'AutomationAccount', '', '', false, '', ''); + +WITH schema_id AS ( + SELECT id FROM schema_extensions WHERE name = 'AZ' +) +INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES + (schema_id, 'AvereContributor', 'AvereContributor', '', true), + (schema_id, 'Contains', 'Contains', '', true), + (schema_id, 'Contributor', 'Contributor', '', true), + (schema_id, 'GetCertificates', 'GetCertificates', '', true), + (schema_id, 'GetKeys', 'GetKeys', '', true), + (schema_id, 'GetSecrets', 'GetSecrets', '', true), + (schema_id, 'HasRole', 'HasRole', '', true), + (schema_id, 'MemberOf', 'MemberOf', '', true), + (schema_id, 'Owner', 'Owner', '', true), + (schema_id, 'RunsAs', 'RunsAs', '', true), + (schema_id, 'VMContributor', 'VMContributor', '', true), + (schema_id, 'AutomationContributor', 'AutomationContributor', '', true), + (schema_id, 'KeyVaultContributor', 'KeyVaultContributor', '', true), + (schema_id, 'VMAdminLogin', 'VMAdminLogin', '', true), + (schema_id, 'AddMembers', 'AddMembers', '', true), + (schema_id, 'AddSecret', 'AddSecret', '', true), + (schema_id, 'ExecuteCommand', 'ExecuteCommand', '', true), + (schema_id, 'GlobalAdmin', 'GlobalAdmin', '', true), + (schema_id, 'PrivilegedAuthAdmin', 'PrivilegedAuthAdmin', '', true), + (schema_id, 'Grant', 'Grant', '', true), + (schema_id, 'GrantSelf', 'GrantSelf', '', true), + (schema_id, 'PrivilegedRoleAdmin', 'PrivilegedRoleAdmin', '', true), + (schema_id, 'ResetPassword', 'ResetPassword', '', true), + (schema_id, 'UserAccessAdministrator', 'UserAccessAdministrator', '', true), + (schema_id, 'Owns', 'Owns', '', true), + (schema_id, 'ScopedTo', 'ScopedTo', '', false), + (schema_id, 'CloudAppAdmin', 'CloudAppAdmin', '', true), + (schema_id, 'AppAdmin', 'AppAdmin', '', true), + (schema_id, 'AddOwner', 'AddOwner', '', true), + (schema_id, 'ManagedIdentity', 'ManagedIdentity', '', true), + (schema_id, 'ApplicationReadWriteAll', 'ApplicationReadWriteAll', '', false), + (schema_id, 'AppRoleAssignmentReadWriteAll', 'AppRoleAssignmentReadWriteAll', '', false), + (schema_id, 'DirectoryReadWriteAll', 'DirectoryReadWriteAll', '', false), + (schema_id, 'GroupReadWriteAll', 'GroupReadWriteAll', '', false), + (schema_id, 'GroupMemberReadWriteAll', 'GroupMemberReadWriteAll', '', false), + (schema_id, 'RoleManagementReadWriteDirectory', 'RoleManagementReadWriteDirectory', '', false), + (schema_id, 'ServicePrincipalEndpointReadWriteAll', 'ServicePrincipalEndpointReadWriteAll', '', false), + (schema_id, 'AKSContributor', 'AKSContributor', '', true), + (schema_id, 'NodeResourceGroup', 'NodeResourceGroup', '', true), + (schema_id, 'WebsiteContributor', 'WebsiteContributor', '', true), + (schema_id, 'LogicAppContributor', 'LogicAppContributor', '', true), + (schema_id, 'AZMGAddMember', 'AZMGAddMember', '', true), + (schema_id, 'AZMGAddOwner', 'AZMGAddOwner', '', true), + (schema_id, 'AZMGAddSecret', 'AZMGAddSecret', '', true), + (schema_id, 'AZMGGrantAppRoles', 'AZMGGrantAppRoles', '', true), + (schema_id, 'AZMGGrantRole', 'AZMGGrantRole', '', true), + (schema_id, 'SyncedToADUser', 'SyncedToADUser', '', true), + (schema_id, 'AZRoleEligible', 'AZRoleEligible', '', true), + (schema_id, 'AZRoleApprover', 'AZRoleApprover', '', true); diff --git a/packages/go/schemagen/generator/cue.go b/packages/go/schemagen/generator/cue.go index 4c970f353ae..c487a807178 100644 --- a/packages/go/schemagen/generator/cue.go +++ b/packages/go/schemagen/generator/cue.go @@ -95,7 +95,10 @@ func (s *ConfigBuilder) OverlayPath(rootPath string) error { } else { overlayPath := filepath.Join(s.overlayRootPath, strings.TrimPrefix(path, rootPath)) - slog.Debug(fmt.Sprintf("Overlaying file: %s to %s", path, overlayPath)) + slog.Debug("Overlaying file", + slog.String("path", path), + slog.String("overlay_path", overlayPath), + ) s.overlay[overlayPath] = load.FromBytes(content) } diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go new file mode 100644 index 00000000000..1fe71ccbbe2 --- /dev/null +++ b/packages/go/schemagen/generator/sql.go @@ -0,0 +1,293 @@ +// 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 generator + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/specterops/bloodhound/packages/go/schemagen/model" +) + +type NodeIcon struct { + Icon string + Color string +} + +var NodeIcons = map[string]NodeIcon{ + // Active Directory Node Types + "User": { + Icon: "fa-user", + Color: "#17E625", + }, + "Group": { + Icon: "fa-users", + Color: "#DBE617", + }, + "Computer": { + Icon: "fa-desktop", + Color: "#E67873", + }, + "Domain": { + Icon: "fa-globe", + Color: "#17E6B9", + }, + "GPO": { + Icon: "fa-list", + Color: "#998EFD", + }, + "AIACA": { + Icon: "fa-arrows-left-right-to-line", + Color: "#9769F0", + }, + "RootCA": { + Icon: "fa-landmark", + Color: "#6968E8", + }, + "EnterpriseCA": { + Icon: "fa-building", + Color: "#4696E9", + }, + "NTAuthStore": { + Icon: "fa-store", + Color: "#D575F5", + }, + "CertTemplate": { + Icon: "fa-id-card", + Color: "#B153F3", + }, + "IssuancePolicy": { + Icon: "fa-clipboard-check", + Color: "#99B2DD", + }, + "OU": { + Icon: "fa-sitemap", + Color: "#FFAA00", + }, + "Container": { + Icon: "fa-box", + Color: "#F79A78", + }, + // Azure Node Types + "AZUser": { + Icon: "fa-user", + Color: "#34D2EB", + }, + "AZGroup": { + Icon: "fa-users", + Color: "#F57C9B", + }, + "AZTenant": { + Icon: "fa-cloud", + Color: "#54F2F2", + }, + "AZSubscription": { + Icon: "fa-key", + Color: "#D2CCA1", + }, + "AZResourceGroup": { + Icon: "fa-cube", + Color: "#89BD9E", + }, + "AZVM": { + Icon: "fa-desktop", + Color: "#F9ADA0", + }, + "AZWebApp": { + Icon: "fa-object-group", + Color: "#4696E9", + }, + "AZLogicApp": { + Icon: "fa-sitemap", + Color: "#9EE047", + }, + "AZAutomationAccount": { + Icon: "fa-cog", + Color: "#F4BA44", + }, + "AZFunctionApp": { + Icon: "fa-bolt", + Color: "#F4BA44", + }, + "AZContainerRegistry": { + Icon: "fa-box-open", + Color: "#0885D7", + }, + "AZManagedCluster": { + Icon: "fa-cubes", + Color: "#326CE5", + }, + "AZDevice": { + Icon: "fa-desktop", + Color: "#B18FCF", + }, + "AZKeyVault": { + Icon: "fa-lock", + Color: "#ED658C", + }, + "AZApp": { + Icon: "fa-window-restore", + Color: "#03FC84", + }, + "AZVMScaleSet": { + Icon: "fa-server", + Color: "#007CD0", + }, + "AZServicePrincipal": { + Icon: "fa-robot", + Color: "#C1D6D6", + }, + "AZRole": { + Icon: "fa-clipboard-list", + Color: "#ED8537", + }, + "AZManagementGroup": { + Icon: "fa-sitemap", + Color: "#BD93D8", + }, +} + +func GenerateExtensionSQLActiveDirectory(dir string, adSchema model.ActiveDirectory) error { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/\n", SchemaSourceName)) + + sb.WriteString("DELETE FROM schema_extensions WHERE name = 'AD';\n\n") + + sb.WriteString("INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true);\n\n") + + sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AD'\n)\nINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") + + for i, kind := range adSchema.NodeKinds { + if iconInfo, found := NodeIcons[kind.Symbol]; found { + sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '%s', '%s')", kind.Symbol, kind.Name, found, iconInfo.Icon, iconInfo.Color)) + } else { + sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '', '')", kind.Symbol, kind.Name, found)) + } + + if i != len(adSchema.NodeKinds)-1 { + sb.WriteString(",\n") + } + } + + sb.WriteString(";\n\n") + + sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AD'\n)\nINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") + + traversableMap := make(map[string]struct{}) + + for _, kind := range adSchema.PathfindingRelationships { + traversableMap[kind.Symbol] = struct{}{} + } + + for i, kind := range adSchema.RelationshipKinds { + _, traversable := traversableMap[kind.Symbol] + + sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t)", kind.Symbol, kind.Symbol, traversable)) + + if i != len(adSchema.RelationshipKinds)-1 { + sb.WriteString(",\n") + } + } + + sb.WriteString(";\n") + + if _, err := os.Stat(dir); err != nil { + if !os.IsNotExist(err) { + return err + } + + if err := os.MkdirAll(dir, defaultPackageDirPermission); err != nil { + return err + } + } + + if fout, err := os.OpenFile(path.Join(dir, "ad.sql"), fileOpenMode, defaultSourceFilePermission); err != nil { + return err + } else { + defer fout.Close() + + _, err := fout.WriteString(sb.String()) + return err + } +} + +func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/\n", SchemaSourceName)) + + sb.WriteString("DELETE FROM schema_extensions WHERE name = 'AZ';\n\n") + + sb.WriteString("INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true);\n\n") + + sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AZ'\n)\nINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") + + for i, kind := range azSchema.NodeKinds { + if iconInfo, found := NodeIcons[kind.Symbol]; found { + sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '%s', '%s')", kind.Symbol, kind.Name, found, iconInfo.Icon, iconInfo.Color)) + } else { + sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '', '')", kind.Symbol, kind.Name, found)) + } + + if i != len(azSchema.NodeKinds)-1 { + sb.WriteString(",\n") + } + } + + sb.WriteString(";\n\n") + + sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AZ'\n)\nINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") + + traversableMap := make(map[string]struct{}) + + for _, kind := range azSchema.PathfindingRelationships { + traversableMap[kind.Symbol] = struct{}{} + } + + for i, kind := range azSchema.RelationshipKinds { + _, traversable := traversableMap[kind.Symbol] + + sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t)", kind.Symbol, kind.Symbol, traversable)) + + if i != len(azSchema.RelationshipKinds)-1 { + sb.WriteString(",\n") + } + } + + sb.WriteString(";\n") + + if _, err := os.Stat(dir); err != nil { + if !os.IsNotExist(err) { + return err + } + + if err := os.MkdirAll(dir, defaultPackageDirPermission); err != nil { + return err + } + } + + if fout, err := os.OpenFile(path.Join(dir, "az.sql"), fileOpenMode, defaultSourceFilePermission); err != nil { + return err + } else { + defer fout.Close() + + _, err := fout.WriteString(sb.String()) + return err + } +} diff --git a/packages/go/schemagen/main.go b/packages/go/schemagen/main.go index 68bdfd1e432..df037cc2d5b 100644 --- a/packages/go/schemagen/main.go +++ b/packages/go/schemagen/main.go @@ -23,6 +23,7 @@ import ( "path/filepath" "cuelang.org/go/cue/errors" + "github.com/specterops/bloodhound/packages/go/bhlog/attr" "github.com/specterops/bloodhound/packages/go/schemagen/generator" "github.com/specterops/bloodhound/packages/go/schemagen/model" "github.com/specterops/bloodhound/packages/go/schemagen/tsgen" @@ -72,24 +73,36 @@ func GenerateCSharp(projectRoot string, rootSchema Schema) error { return generator.GenerateCSharpBindings(projectRoot, rootSchema.Common, rootSchema.ActiveDirectory) } +func GenerateSQL(projectRoot string, rootSchema Schema) error { + if err := generator.GenerateExtensionSQLActiveDirectory(filepath.Join(projectRoot, "cmd/api/src/database/migration/extensions"), rootSchema.ActiveDirectory); err != nil { + return err + } + + if err := generator.GenerateExtensionSQLAzure(filepath.Join(projectRoot, "cmd/api/src/database/migration/extensions"), rootSchema.Azure); err != nil { + return err + } + + return nil +} + func main() { cfgBuilder := generator.NewConfigBuilder("/schemas") if projectRoot, err := generator.FindGolangWorkspaceRoot(); err != nil { - slog.Error(fmt.Sprintf("Error finding project root: %v", err)) + slog.Error("Error finding project root", attr.Error(err)) os.Exit(1) } else { - slog.Info(fmt.Sprintf("Project root is %s", projectRoot)) + slog.Info("Found project root", slog.String("project_root", projectRoot)) if err := cfgBuilder.OverlayPath(filepath.Join(projectRoot, "packages/cue")); err != nil { - slog.Error(fmt.Sprintf("Error: %v", err)) + slog.Error("Failed to read overlay path", attr.Error(err)) os.Exit(1) } cfg := cfgBuilder.Build() if bhInstance, err := cfg.Value("/schemas/bh/bh.cue"); err != nil { - slog.Error(fmt.Sprintf("Error: %v", errors.Details(err, nil))) + slog.Error("Failed to load cue schema", slog.String("err", errors.Details(err, nil))) os.Exit(1) } else { var bhModels Schema @@ -113,6 +126,11 @@ func main() { slog.Error(fmt.Sprintf("Error %v", err)) os.Exit(1) } + + if err := GenerateSQL(projectRoot, bhModels); err != nil { + slog.Error(fmt.Sprintf("Error %v", err)) + os.Exit(1) + } } } } From 51bf581b1cc9a6b03994d9eff971ed3c2bb18130 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:59:45 -0500 Subject: [PATCH 02/27] BED-6721: make it work more good --- .../src/database/migration/extensions/ad.sql | 221 +++++++++--------- .../src/database/migration/extensions/az.sql | 155 ++++++------ packages/go/schemagen/generator/sql.go | 32 +-- 3 files changed, 205 insertions(+), 203 deletions(-) diff --git a/cmd/api/src/database/migration/extensions/ad.sql b/cmd/api/src/database/migration/extensions/ad.sql index 0903e74e842..443a3748d84 100644 --- a/cmd/api/src/database/migration/extensions/ad.sql +++ b/cmd/api/src/database/migration/extensions/ad.sql @@ -17,116 +17,115 @@ -- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ DELETE FROM schema_extensions WHERE name = 'AD'; -INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true); +DO $$ +DECLARE + new_extension_id INT; +BEGIN + INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true) RETURNING id INTO new_extension_id; -WITH schema_id AS ( - SELECT id FROM schema_extensions WHERE name = 'AD' -) -INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES - (schema_id, 'Entity', '', '', false, '', ''), - (schema_id, 'User', '', '', true, 'fa-user', '#17E625'), - (schema_id, 'Computer', '', '', true, 'fa-desktop', '#E67873'), - (schema_id, 'Group', '', '', true, 'fa-users', '#DBE617'), - (schema_id, 'GPO', '', '', true, 'fa-list', '#998EFD'), - (schema_id, 'OU', '', '', true, 'fa-sitemap', '#FFAA00'), - (schema_id, 'Container', '', '', true, 'fa-box', '#F79A78'), - (schema_id, 'Domain', '', '', true, 'fa-globe', '#17E6B9'), - (schema_id, 'LocalGroup', '', '', false, '', ''), - (schema_id, 'LocalUser', '', '', false, '', ''), - (schema_id, 'AIACA', '', '', true, 'fa-arrows-left-right-to-line', '#9769F0'), - (schema_id, 'RootCA', '', '', true, 'fa-landmark', '#6968E8'), - (schema_id, 'EnterpriseCA', '', '', true, 'fa-building', '#4696E9'), - (schema_id, 'NTAuthStore', '', '', true, 'fa-store', '#D575F5'), - (schema_id, 'CertTemplate', '', '', true, 'fa-id-card', '#B153F3'), - (schema_id, 'IssuancePolicy', '', '', true, 'fa-clipboard-check', '#99B2DD'); + INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES + (new_extension_id, 'Base', 'Entity', '', false, '', ''), + (new_extension_id, 'User', 'User', '', true, 'fa-user', '#17E625'), + (new_extension_id, 'Computer', 'Computer', '', true, 'fa-desktop', '#E67873'), + (new_extension_id, 'Group', 'Group', '', true, 'fa-users', '#DBE617'), + (new_extension_id, 'GPO', 'GPO', '', true, 'fa-list', '#998EFD'), + (new_extension_id, 'OU', 'OU', '', true, 'fa-sitemap', '#FFAA00'), + (new_extension_id, 'Container', 'Container', '', true, 'fa-box', '#F79A78'), + (new_extension_id, 'Domain', 'Domain', '', true, 'fa-globe', '#17E6B9'), + (new_extension_id, 'ADLocalGroup', 'LocalGroup', '', false, '', ''), + (new_extension_id, 'ADLocalUser', 'LocalUser', '', false, '', ''), + (new_extension_id, 'AIACA', 'AIACA', '', true, 'fa-arrows-left-right-to-line', '#9769F0'), + (new_extension_id, 'RootCA', 'RootCA', '', true, 'fa-landmark', '#6968E8'), + (new_extension_id, 'EnterpriseCA', 'EnterpriseCA', '', true, 'fa-building', '#4696E9'), + (new_extension_id, 'NTAuthStore', 'NTAuthStore', '', true, 'fa-store', '#D575F5'), + (new_extension_id, 'CertTemplate', 'CertTemplate', '', true, 'fa-id-card', '#B153F3'), + (new_extension_id, 'IssuancePolicy', 'IssuancePolicy', '', true, 'fa-clipboard-check', '#99B2DD'); -WITH schema_id AS ( - SELECT id FROM schema_extensions WHERE name = 'AD' -) -INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES - (schema_id, 'Owns', 'Owns', '', true), - (schema_id, 'GenericAll', 'GenericAll', '', true), - (schema_id, 'GenericWrite', 'GenericWrite', '', true), - (schema_id, 'WriteOwner', 'WriteOwner', '', true), - (schema_id, 'WriteDACL', 'WriteDACL', '', true), - (schema_id, 'MemberOf', 'MemberOf', '', true), - (schema_id, 'ForceChangePassword', 'ForceChangePassword', '', true), - (schema_id, 'AllExtendedRights', 'AllExtendedRights', '', true), - (schema_id, 'AddMember', 'AddMember', '', true), - (schema_id, 'HasSession', 'HasSession', '', true), - (schema_id, 'Contains', 'Contains', '', true), - (schema_id, 'GPLink', 'GPLink', '', true), - (schema_id, 'AllowedToDelegate', 'AllowedToDelegate', '', true), - (schema_id, 'CoerceToTGT', 'CoerceToTGT', '', true), - (schema_id, 'GetChanges', 'GetChanges', '', false), - (schema_id, 'GetChangesAll', 'GetChangesAll', '', false), - (schema_id, 'GetChangesInFilteredSet', 'GetChangesInFilteredSet', '', false), - (schema_id, 'CrossForestTrust', 'CrossForestTrust', '', false), - (schema_id, 'SameForestTrust', 'SameForestTrust', '', true), - (schema_id, 'SpoofSIDHistory', 'SpoofSIDHistory', '', true), - (schema_id, 'AbuseTGTDelegation', 'AbuseTGTDelegation', '', true), - (schema_id, 'AllowedToAct', 'AllowedToAct', '', true), - (schema_id, 'AdminTo', 'AdminTo', '', true), - (schema_id, 'CanPSRemote', 'CanPSRemote', '', true), - (schema_id, 'CanRDP', 'CanRDP', '', true), - (schema_id, 'ExecuteDCOM', 'ExecuteDCOM', '', true), - (schema_id, 'HasSIDHistory', 'HasSIDHistory', '', true), - (schema_id, 'AddSelf', 'AddSelf', '', true), - (schema_id, 'DCSync', 'DCSync', '', true), - (schema_id, 'ReadLAPSPassword', 'ReadLAPSPassword', '', true), - (schema_id, 'ReadGMSAPassword', 'ReadGMSAPassword', '', true), - (schema_id, 'DumpSMSAPassword', 'DumpSMSAPassword', '', true), - (schema_id, 'SQLAdmin', 'SQLAdmin', '', true), - (schema_id, 'AddAllowedToAct', 'AddAllowedToAct', '', true), - (schema_id, 'WriteSPN', 'WriteSPN', '', true), - (schema_id, 'AddKeyCredentialLink', 'AddKeyCredentialLink', '', true), - (schema_id, 'LocalToComputer', 'LocalToComputer', '', false), - (schema_id, 'MemberOfLocalGroup', 'MemberOfLocalGroup', '', false), - (schema_id, 'RemoteInteractiveLogonRight', 'RemoteInteractiveLogonRight', '', false), - (schema_id, 'SyncLAPSPassword', 'SyncLAPSPassword', '', true), - (schema_id, 'WriteAccountRestrictions', 'WriteAccountRestrictions', '', true), - (schema_id, 'WriteGPLink', 'WriteGPLink', '', true), - (schema_id, 'RootCAFor', 'RootCAFor', '', false), - (schema_id, 'DCFor', 'DCFor', '', true), - (schema_id, 'PublishedTo', 'PublishedTo', '', false), - (schema_id, 'ManageCertificates', 'ManageCertificates', '', true), - (schema_id, 'ManageCA', 'ManageCA', '', true), - (schema_id, 'DelegatedEnrollmentAgent', 'DelegatedEnrollmentAgent', '', false), - (schema_id, 'Enroll', 'Enroll', '', false), - (schema_id, 'HostsCAService', 'HostsCAService', '', false), - (schema_id, 'WritePKIEnrollmentFlag', 'WritePKIEnrollmentFlag', '', false), - (schema_id, 'WritePKINameFlag', 'WritePKINameFlag', '', false), - (schema_id, 'NTAuthStoreFor', 'NTAuthStoreFor', '', false), - (schema_id, 'TrustedForNTAuth', 'TrustedForNTAuth', '', false), - (schema_id, 'EnterpriseCAFor', 'EnterpriseCAFor', '', false), - (schema_id, 'IssuedSignedBy', 'IssuedSignedBy', '', false), - (schema_id, 'GoldenCert', 'GoldenCert', '', true), - (schema_id, 'EnrollOnBehalfOf', 'EnrollOnBehalfOf', '', false), - (schema_id, 'OIDGroupLink', 'OIDGroupLink', '', false), - (schema_id, 'ExtendedByPolicy', 'ExtendedByPolicy', '', false), - (schema_id, 'ADCSESC1', 'ADCSESC1', '', true), - (schema_id, 'ADCSESC3', 'ADCSESC3', '', true), - (schema_id, 'ADCSESC4', 'ADCSESC4', '', true), - (schema_id, 'ADCSESC6a', 'ADCSESC6a', '', true), - (schema_id, 'ADCSESC6b', 'ADCSESC6b', '', true), - (schema_id, 'ADCSESC9a', 'ADCSESC9a', '', true), - (schema_id, 'ADCSESC9b', 'ADCSESC9b', '', true), - (schema_id, 'ADCSESC10a', 'ADCSESC10a', '', true), - (schema_id, 'ADCSESC10b', 'ADCSESC10b', '', true), - (schema_id, 'ADCSESC13', 'ADCSESC13', '', true), - (schema_id, 'SyncedToEntraUser', 'SyncedToEntraUser', '', true), - (schema_id, 'CoerceAndRelayNTLMToSMB', 'CoerceAndRelayNTLMToSMB', '', true), - (schema_id, 'CoerceAndRelayNTLMToADCS', 'CoerceAndRelayNTLMToADCS', '', true), - (schema_id, 'WriteOwnerLimitedRights', 'WriteOwnerLimitedRights', '', true), - (schema_id, 'WriteOwnerRaw', 'WriteOwnerRaw', '', false), - (schema_id, 'OwnsLimitedRights', 'OwnsLimitedRights', '', true), - (schema_id, 'OwnsRaw', 'OwnsRaw', '', false), - (schema_id, 'ClaimSpecialIdentity', 'ClaimSpecialIdentity', '', true), - (schema_id, 'CoerceAndRelayNTLMToLDAP', 'CoerceAndRelayNTLMToLDAP', '', true), - (schema_id, 'CoerceAndRelayNTLMToLDAPS', 'CoerceAndRelayNTLMToLDAPS', '', true), - (schema_id, 'ContainsIdentity', 'ContainsIdentity', '', true), - (schema_id, 'PropagatesACEsTo', 'PropagatesACEsTo', '', true), - (schema_id, 'GPOAppliesTo', 'GPOAppliesTo', '', true), - (schema_id, 'CanApplyGPO', 'CanApplyGPO', '', true), - (schema_id, 'HasTrustKeys', 'HasTrustKeys', '', true), - (schema_id, 'ProtectAdminGroups', 'ProtectAdminGroups', '', false); + INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES + (new_extension_id, 'Owns', '', true), + (new_extension_id, 'GenericAll', '', true), + (new_extension_id, 'GenericWrite', '', true), + (new_extension_id, 'WriteOwner', '', true), + (new_extension_id, 'WriteDacl', '', true), + (new_extension_id, 'MemberOf', '', true), + (new_extension_id, 'ForceChangePassword', '', true), + (new_extension_id, 'AllExtendedRights', '', true), + (new_extension_id, 'AddMember', '', true), + (new_extension_id, 'HasSession', '', true), + (new_extension_id, 'Contains', '', true), + (new_extension_id, 'GPLink', '', true), + (new_extension_id, 'AllowedToDelegate', '', true), + (new_extension_id, 'CoerceToTGT', '', true), + (new_extension_id, 'GetChanges', '', false), + (new_extension_id, 'GetChangesAll', '', false), + (new_extension_id, 'GetChangesInFilteredSet', '', false), + (new_extension_id, 'CrossForestTrust', '', false), + (new_extension_id, 'SameForestTrust', '', true), + (new_extension_id, 'SpoofSIDHistory', '', true), + (new_extension_id, 'AbuseTGTDelegation', '', true), + (new_extension_id, 'AllowedToAct', '', true), + (new_extension_id, 'AdminTo', '', true), + (new_extension_id, 'CanPSRemote', '', true), + (new_extension_id, 'CanRDP', '', true), + (new_extension_id, 'ExecuteDCOM', '', true), + (new_extension_id, 'HasSIDHistory', '', true), + (new_extension_id, 'AddSelf', '', true), + (new_extension_id, 'DCSync', '', true), + (new_extension_id, 'ReadLAPSPassword', '', true), + (new_extension_id, 'ReadGMSAPassword', '', true), + (new_extension_id, 'DumpSMSAPassword', '', true), + (new_extension_id, 'SQLAdmin', '', true), + (new_extension_id, 'AddAllowedToAct', '', true), + (new_extension_id, 'WriteSPN', '', true), + (new_extension_id, 'AddKeyCredentialLink', '', true), + (new_extension_id, 'LocalToComputer', '', false), + (new_extension_id, 'MemberOfLocalGroup', '', false), + (new_extension_id, 'RemoteInteractiveLogonRight', '', false), + (new_extension_id, 'SyncLAPSPassword', '', true), + (new_extension_id, 'WriteAccountRestrictions', '', true), + (new_extension_id, 'WriteGPLink', '', true), + (new_extension_id, 'RootCAFor', '', false), + (new_extension_id, 'DCFor', '', true), + (new_extension_id, 'PublishedTo', '', false), + (new_extension_id, 'ManageCertificates', '', true), + (new_extension_id, 'ManageCA', '', true), + (new_extension_id, 'DelegatedEnrollmentAgent', '', false), + (new_extension_id, 'Enroll', '', false), + (new_extension_id, 'HostsCAService', '', false), + (new_extension_id, 'WritePKIEnrollmentFlag', '', false), + (new_extension_id, 'WritePKINameFlag', '', false), + (new_extension_id, 'NTAuthStoreFor', '', false), + (new_extension_id, 'TrustedForNTAuth', '', false), + (new_extension_id, 'EnterpriseCAFor', '', false), + (new_extension_id, 'IssuedSignedBy', '', false), + (new_extension_id, 'GoldenCert', '', true), + (new_extension_id, 'EnrollOnBehalfOf', '', false), + (new_extension_id, 'OIDGroupLink', '', false), + (new_extension_id, 'ExtendedByPolicy', '', false), + (new_extension_id, 'ADCSESC1', '', true), + (new_extension_id, 'ADCSESC3', '', true), + (new_extension_id, 'ADCSESC4', '', true), + (new_extension_id, 'ADCSESC6a', '', true), + (new_extension_id, 'ADCSESC6b', '', true), + (new_extension_id, 'ADCSESC9a', '', true), + (new_extension_id, 'ADCSESC9b', '', true), + (new_extension_id, 'ADCSESC10a', '', true), + (new_extension_id, 'ADCSESC10b', '', true), + (new_extension_id, 'ADCSESC13', '', true), + (new_extension_id, 'SyncedToEntraUser', '', true), + (new_extension_id, 'CoerceAndRelayNTLMToSMB', '', true), + (new_extension_id, 'CoerceAndRelayNTLMToADCS', '', true), + (new_extension_id, 'WriteOwnerLimitedRights', '', true), + (new_extension_id, 'WriteOwnerRaw', '', false), + (new_extension_id, 'OwnsLimitedRights', '', true), + (new_extension_id, 'OwnsRaw', '', false), + (new_extension_id, 'ClaimSpecialIdentity', '', true), + (new_extension_id, 'CoerceAndRelayNTLMToLDAP', '', true), + (new_extension_id, 'CoerceAndRelayNTLMToLDAPS', '', true), + (new_extension_id, 'ContainsIdentity', '', true), + (new_extension_id, 'PropagatesACEsTo', '', true), + (new_extension_id, 'GPOAppliesTo', '', true), + (new_extension_id, 'CanApplyGPO', '', true), + (new_extension_id, 'HasTrustKeys', '', true), + (new_extension_id, 'ProtectAdminGroups', '', false); +END $$; diff --git a/cmd/api/src/database/migration/extensions/az.sql b/cmd/api/src/database/migration/extensions/az.sql index aac87492fad..1e2c62065ac 100644 --- a/cmd/api/src/database/migration/extensions/az.sql +++ b/cmd/api/src/database/migration/extensions/az.sql @@ -17,83 +17,82 @@ -- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ DELETE FROM schema_extensions WHERE name = 'AZ'; -INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true); +DO $$ +DECLARE + new_extension_id INT; +BEGIN + INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true) RETURNING id INTO new_extension_id; -WITH schema_id AS ( - SELECT id FROM schema_extensions WHERE name = 'AZ' -) -INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES - (schema_id, 'Entity', '', '', false, '', ''), - (schema_id, 'VMScaleSet', '', '', false, '', ''), - (schema_id, 'App', '', '', false, '', ''), - (schema_id, 'Role', '', '', false, '', ''), - (schema_id, 'Device', '', '', false, '', ''), - (schema_id, 'FunctionApp', '', '', false, '', ''), - (schema_id, 'Group', '', '', true, 'fa-users', '#DBE617'), - (schema_id, 'KeyVault', '', '', false, '', ''), - (schema_id, 'ManagementGroup', '', '', false, '', ''), - (schema_id, 'ResourceGroup', '', '', false, '', ''), - (schema_id, 'ServicePrincipal', '', '', false, '', ''), - (schema_id, 'Subscription', '', '', false, '', ''), - (schema_id, 'Tenant', '', '', false, '', ''), - (schema_id, 'User', '', '', true, 'fa-user', '#17E625'), - (schema_id, 'VM', '', '', false, '', ''), - (schema_id, 'ManagedCluster', '', '', false, '', ''), - (schema_id, 'ContainerRegistry', '', '', false, '', ''), - (schema_id, 'WebApp', '', '', false, '', ''), - (schema_id, 'LogicApp', '', '', false, '', ''), - (schema_id, 'AutomationAccount', '', '', false, '', ''); + INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES + (new_extension_id, 'AZBase', 'Entity', '', false, '', ''), + (new_extension_id, 'AZVMScaleSet', 'VMScaleSet', '', false, '', ''), + (new_extension_id, 'AZApp', 'App', '', false, '', ''), + (new_extension_id, 'AZRole', 'Role', '', false, '', ''), + (new_extension_id, 'AZDevice', 'Device', '', false, '', ''), + (new_extension_id, 'AZFunctionApp', 'FunctionApp', '', false, '', ''), + (new_extension_id, 'AZGroup', 'Group', '', true, 'fa-users', '#DBE617'), + (new_extension_id, 'AZKeyVault', 'KeyVault', '', false, '', ''), + (new_extension_id, 'AZManagementGroup', 'ManagementGroup', '', false, '', ''), + (new_extension_id, 'AZResourceGroup', 'ResourceGroup', '', false, '', ''), + (new_extension_id, 'AZServicePrincipal', 'ServicePrincipal', '', false, '', ''), + (new_extension_id, 'AZSubscription', 'Subscription', '', false, '', ''), + (new_extension_id, 'AZTenant', 'Tenant', '', false, '', ''), + (new_extension_id, 'AZUser', 'User', '', true, 'fa-user', '#17E625'), + (new_extension_id, 'AZVM', 'VM', '', false, '', ''), + (new_extension_id, 'AZManagedCluster', 'ManagedCluster', '', false, '', ''), + (new_extension_id, 'AZContainerRegistry', 'ContainerRegistry', '', false, '', ''), + (new_extension_id, 'AZWebApp', 'WebApp', '', false, '', ''), + (new_extension_id, 'AZLogicApp', 'LogicApp', '', false, '', ''), + (new_extension_id, 'AZAutomationAccount', 'AutomationAccount', '', false, '', ''); -WITH schema_id AS ( - SELECT id FROM schema_extensions WHERE name = 'AZ' -) -INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES - (schema_id, 'AvereContributor', 'AvereContributor', '', true), - (schema_id, 'Contains', 'Contains', '', true), - (schema_id, 'Contributor', 'Contributor', '', true), - (schema_id, 'GetCertificates', 'GetCertificates', '', true), - (schema_id, 'GetKeys', 'GetKeys', '', true), - (schema_id, 'GetSecrets', 'GetSecrets', '', true), - (schema_id, 'HasRole', 'HasRole', '', true), - (schema_id, 'MemberOf', 'MemberOf', '', true), - (schema_id, 'Owner', 'Owner', '', true), - (schema_id, 'RunsAs', 'RunsAs', '', true), - (schema_id, 'VMContributor', 'VMContributor', '', true), - (schema_id, 'AutomationContributor', 'AutomationContributor', '', true), - (schema_id, 'KeyVaultContributor', 'KeyVaultContributor', '', true), - (schema_id, 'VMAdminLogin', 'VMAdminLogin', '', true), - (schema_id, 'AddMembers', 'AddMembers', '', true), - (schema_id, 'AddSecret', 'AddSecret', '', true), - (schema_id, 'ExecuteCommand', 'ExecuteCommand', '', true), - (schema_id, 'GlobalAdmin', 'GlobalAdmin', '', true), - (schema_id, 'PrivilegedAuthAdmin', 'PrivilegedAuthAdmin', '', true), - (schema_id, 'Grant', 'Grant', '', true), - (schema_id, 'GrantSelf', 'GrantSelf', '', true), - (schema_id, 'PrivilegedRoleAdmin', 'PrivilegedRoleAdmin', '', true), - (schema_id, 'ResetPassword', 'ResetPassword', '', true), - (schema_id, 'UserAccessAdministrator', 'UserAccessAdministrator', '', true), - (schema_id, 'Owns', 'Owns', '', true), - (schema_id, 'ScopedTo', 'ScopedTo', '', false), - (schema_id, 'CloudAppAdmin', 'CloudAppAdmin', '', true), - (schema_id, 'AppAdmin', 'AppAdmin', '', true), - (schema_id, 'AddOwner', 'AddOwner', '', true), - (schema_id, 'ManagedIdentity', 'ManagedIdentity', '', true), - (schema_id, 'ApplicationReadWriteAll', 'ApplicationReadWriteAll', '', false), - (schema_id, 'AppRoleAssignmentReadWriteAll', 'AppRoleAssignmentReadWriteAll', '', false), - (schema_id, 'DirectoryReadWriteAll', 'DirectoryReadWriteAll', '', false), - (schema_id, 'GroupReadWriteAll', 'GroupReadWriteAll', '', false), - (schema_id, 'GroupMemberReadWriteAll', 'GroupMemberReadWriteAll', '', false), - (schema_id, 'RoleManagementReadWriteDirectory', 'RoleManagementReadWriteDirectory', '', false), - (schema_id, 'ServicePrincipalEndpointReadWriteAll', 'ServicePrincipalEndpointReadWriteAll', '', false), - (schema_id, 'AKSContributor', 'AKSContributor', '', true), - (schema_id, 'NodeResourceGroup', 'NodeResourceGroup', '', true), - (schema_id, 'WebsiteContributor', 'WebsiteContributor', '', true), - (schema_id, 'LogicAppContributor', 'LogicAppContributor', '', true), - (schema_id, 'AZMGAddMember', 'AZMGAddMember', '', true), - (schema_id, 'AZMGAddOwner', 'AZMGAddOwner', '', true), - (schema_id, 'AZMGAddSecret', 'AZMGAddSecret', '', true), - (schema_id, 'AZMGGrantAppRoles', 'AZMGGrantAppRoles', '', true), - (schema_id, 'AZMGGrantRole', 'AZMGGrantRole', '', true), - (schema_id, 'SyncedToADUser', 'SyncedToADUser', '', true), - (schema_id, 'AZRoleEligible', 'AZRoleEligible', '', true), - (schema_id, 'AZRoleApprover', 'AZRoleApprover', '', true); + INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES + (new_extension_id, 'AZAvereContributor', '', true), + (new_extension_id, 'AZContains', '', true), + (new_extension_id, 'AZContributor', '', true), + (new_extension_id, 'AZGetCertificates', '', true), + (new_extension_id, 'AZGetKeys', '', true), + (new_extension_id, 'AZGetSecrets', '', true), + (new_extension_id, 'AZHasRole', '', true), + (new_extension_id, 'AZMemberOf', '', true), + (new_extension_id, 'AZOwner', '', true), + (new_extension_id, 'AZRunsAs', '', true), + (new_extension_id, 'AZVMContributor', '', true), + (new_extension_id, 'AZAutomationContributor', '', true), + (new_extension_id, 'AZKeyVaultContributor', '', true), + (new_extension_id, 'AZVMAdminLogin', '', true), + (new_extension_id, 'AZAddMembers', '', true), + (new_extension_id, 'AZAddSecret', '', true), + (new_extension_id, 'AZExecuteCommand', '', true), + (new_extension_id, 'AZGlobalAdmin', '', true), + (new_extension_id, 'AZPrivilegedAuthAdmin', '', true), + (new_extension_id, 'AZGrant', '', true), + (new_extension_id, 'AZGrantSelf', '', true), + (new_extension_id, 'AZPrivilegedRoleAdmin', '', true), + (new_extension_id, 'AZResetPassword', '', true), + (new_extension_id, 'AZUserAccessAdministrator', '', true), + (new_extension_id, 'AZOwns', '', true), + (new_extension_id, 'AZScopedTo', '', false), + (new_extension_id, 'AZCloudAppAdmin', '', true), + (new_extension_id, 'AZAppAdmin', '', true), + (new_extension_id, 'AZAddOwner', '', true), + (new_extension_id, 'AZManagedIdentity', '', true), + (new_extension_id, 'AZMGApplication_ReadWrite_All', '', false), + (new_extension_id, 'AZMGAppRoleAssignment_ReadWrite_All', '', false), + (new_extension_id, 'AZMGDirectory_ReadWrite_All', '', false), + (new_extension_id, 'AZMGGroup_ReadWrite_All', '', false), + (new_extension_id, 'AZMGGroupMember_ReadWrite_All', '', false), + (new_extension_id, 'AZMGRoleManagement_ReadWrite_Directory', '', false), + (new_extension_id, 'AZMGServicePrincipalEndpoint_ReadWrite_All', '', false), + (new_extension_id, 'AZAKSContributor', '', true), + (new_extension_id, 'AZNodeResourceGroup', '', true), + (new_extension_id, 'AZWebsiteContributor', '', true), + (new_extension_id, 'AZLogicAppContributor', '', true), + (new_extension_id, 'AZMGAddMember', '', true), + (new_extension_id, 'AZMGAddOwner', '', true), + (new_extension_id, 'AZMGAddSecret', '', true), + (new_extension_id, 'AZMGGrantAppRoles', '', true), + (new_extension_id, 'AZMGGrantRole', '', true), + (new_extension_id, 'SyncedToADUser', '', true), + (new_extension_id, 'AZRoleEligible', '', true), + (new_extension_id, 'AZRoleApprover', '', true); +END $$; diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 1fe71ccbbe2..79a285e0307 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -169,15 +169,17 @@ func GenerateExtensionSQLActiveDirectory(dir string, adSchema model.ActiveDirect sb.WriteString("DELETE FROM schema_extensions WHERE name = 'AD';\n\n") - sb.WriteString("INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true);\n\n") + sb.WriteString("DO $$\nDECLARE\n\tnew_extension_id INT;\nBEGIN\n") - sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AD'\n)\nINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") + sb.WriteString("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true) RETURNING id INTO new_extension_id;\n\n") + + sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") for i, kind := range adSchema.NodeKinds { if iconInfo, found := NodeIcons[kind.Symbol]; found { - sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '%s', '%s')", kind.Symbol, kind.Name, found, iconInfo.Icon, iconInfo.Color)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) } else { - sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '', '')", kind.Symbol, kind.Name, found)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) } if i != len(adSchema.NodeKinds)-1 { @@ -187,7 +189,7 @@ func GenerateExtensionSQLActiveDirectory(dir string, adSchema model.ActiveDirect sb.WriteString(";\n\n") - sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AD'\n)\nINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") + sb.WriteString("\tINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") traversableMap := make(map[string]struct{}) @@ -198,14 +200,14 @@ func GenerateExtensionSQLActiveDirectory(dir string, adSchema model.ActiveDirect for i, kind := range adSchema.RelationshipKinds { _, traversable := traversableMap[kind.Symbol] - sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t)", kind.Symbol, kind.Symbol, traversable)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '', %t)", kind.GetRepresentation(), traversable)) if i != len(adSchema.RelationshipKinds)-1 { sb.WriteString(",\n") } } - sb.WriteString(";\n") + sb.WriteString(";\nEND $$;") if _, err := os.Stat(dir); err != nil { if !os.IsNotExist(err) { @@ -234,15 +236,17 @@ func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { sb.WriteString("DELETE FROM schema_extensions WHERE name = 'AZ';\n\n") - sb.WriteString("INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true);\n\n") + sb.WriteString("DO $$\nDECLARE\n\tnew_extension_id INT;\nBEGIN\n") + + sb.WriteString("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true) RETURNING id INTO new_extension_id;\n\n") - sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AZ'\n)\nINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") + sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") for i, kind := range azSchema.NodeKinds { if iconInfo, found := NodeIcons[kind.Symbol]; found { - sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '%s', '%s')", kind.Symbol, kind.Name, found, iconInfo.Icon, iconInfo.Color)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) } else { - sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t, '', '')", kind.Symbol, kind.Name, found)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) } if i != len(azSchema.NodeKinds)-1 { @@ -252,7 +256,7 @@ func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { sb.WriteString(";\n\n") - sb.WriteString("WITH schema_id AS (\n\tSELECT id FROM schema_extensions WHERE name = 'AZ'\n)\nINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") + sb.WriteString("\tINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") traversableMap := make(map[string]struct{}) @@ -263,14 +267,14 @@ func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { for i, kind := range azSchema.RelationshipKinds { _, traversable := traversableMap[kind.Symbol] - sb.WriteString(fmt.Sprintf("\t(schema_id, '%s', '%s', '', %t)", kind.Symbol, kind.Symbol, traversable)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '', %t)", kind.GetRepresentation(), traversable)) if i != len(azSchema.RelationshipKinds)-1 { sb.WriteString(",\n") } } - sb.WriteString(";\n") + sb.WriteString(";\nEND $$;") if _, err := os.Stat(dir); err != nil { if !os.IsNotExist(err) { From 02f4c4506ab832735d2dc52076b1dc2bc86bcedd Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:00:54 -0700 Subject: [PATCH 03/27] fk node and edge schema kind tables to DAWGS kind table --- cmd/api/src/database/graphschema.go | 67 ++++++++++++++----- .../database/graphschema_integration_test.go | 43 ++++++++---- .../database/migration/migrations/v8.5.0.sql | 19 ++++-- 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index a8346f1e7a3..69c709c352f 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -53,7 +53,9 @@ type OpenGraphSchema interface { GetSchemaEnvironments(ctx context.Context) ([]model.SchemaEnvironment, error) } -const DuplicateKeyValueErrorString = "duplicate key value violates unique constraint" +const ( + DuplicateKeyValueErrorString = "duplicate key value violates unique constraint" +) // CreateGraphSchemaExtension creates a new row in the extensions table. A GraphSchemaExtension struct is returned, populated with the value as it stands in the database. func (s *BloodhoundDB) CreateGraphSchemaExtension(ctx context.Context, name string, displayName string, version string) (model.GraphSchemaExtension, error) { @@ -198,16 +200,24 @@ func (s *BloodhoundDB) DeleteGraphSchemaExtension(ctx context.Context, extension return nil } -// CreateGraphSchemaNodeKind - creates a new row in the schema_node_kinds table. A model.GraphSchemaNodeKind struct is returned, populated with the value as it stands in the database. +// CreateGraphSchemaNodeKind - creates a new row in the schema_node_kinds table. A model.GraphSchemaNodeKind struct is +// returned, populated with the value as it stands in the database. This will also create a kind in the DAWGS kind table +// if the kind does not already exist. Since this inserts directly into the kinds table, the business logic calling this func +// must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaNodeKind(ctx context.Context, name string, extensionId int32, displayName string, description string, isDisplayKind bool, icon, iconColor string) (model.GraphSchemaNodeKind, error) { schemaNodeKind := model.GraphSchemaNodeKind{} + // DO UPDATE forces the CTE to return the id and name, nothing would be returned if DO NOTHING is used if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` - INSERT INTO %s (name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color) - VALUES (?, ?, ?, ?, ?, ?, ?) - RETURNING id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at`, - schemaNodeKind.TableName()), - name, extensionId, displayName, description, isDisplayKind, icon, iconColor).Scan(&schemaNodeKind); result.Error != nil { + WITH dawgs_kinds AS ( + INSERT INTO %s (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? + RETURNING id, name + ) + INSERT INTO %s (id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color) + SELECT id, name, ?, ?, ?, ?, ?, ? + FROM dawgs_kinds + RETURNING id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at;`, + kindTable, schemaNodeKind.TableName()), name, name, extensionId, displayName, description, isDisplayKind, icon, iconColor).Scan(&schemaNodeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, result.Error) } @@ -283,14 +293,17 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKindById(ctx context.Context, schemaNod FROM %s WHERE id = ?`, schemaNodeKind.TableName()), schemaNodeKindId).First(&schemaNodeKind)) } -// UpdateGraphSchemaNodeKind - 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. +// UpdateGraphSchemaNodeKind - 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. +// A model.GraphSchemaNodeKind cannot have its name updated due to FK'ing to the DAWGS kind table. A new node kind should +// be created if the names differ. func (s *BloodhoundDB) UpdateGraphSchemaNodeKind(ctx context.Context, schemaNodeKind model.GraphSchemaNodeKind) (model.GraphSchemaNodeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` UPDATE %s - SET name = ?, schema_extension_id = ?, display_name = ?, description = ?, is_display_kind = ?, icon = ?, icon_color = ?, updated_at = NOW() + SET 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`, - schemaNodeKind.TableName()), schemaNodeKind.Name, schemaNodeKind.SchemaExtensionId, schemaNodeKind.DisplayName, schemaNodeKind.Description, schemaNodeKind.IsDisplayKind, schemaNodeKind.Icon, schemaNodeKind.IconColor, schemaNodeKind.ID).Scan(&schemaNodeKind); result.Error != nil { + schemaNodeKind.TableName()), schemaNodeKind.SchemaExtensionId, schemaNodeKind.DisplayName, schemaNodeKind.Description, schemaNodeKind.IsDisplayKind, schemaNodeKind.Icon, schemaNodeKind.IconColor, schemaNodeKind.ID).Scan(&schemaNodeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, result.Error) } @@ -406,6 +419,7 @@ func (s *BloodhoundDB) GetGraphSchemaPropertyById(ctx context.Context, extension return extensionProperty, nil } +// UpdateGraphSchemaProperty - updates a row in the schema_property table based on the provided id. It will return an error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. 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 = ?, schema_extension_id = ?, display_name = ?, data_type = ?, description = ?, updated_at = NOW() WHERE id = ? @@ -423,6 +437,7 @@ func (s *BloodhoundDB) UpdateGraphSchemaProperty(ctx context.Context, property m return property, nil } +// DeleteGraphSchemaProperty - deletes a schema_property row based on the provided id. It will return an error if that id does not exist. func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID int32) error { var property model.GraphSchemaProperty @@ -437,15 +452,24 @@ func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID return nil } -// CreateGraphSchemaEdgeKind - creates a new row in the schema_edge_kinds table. A model.GraphSchemaEdgeKind struct is returned, populated with the value as it stands in the database. +// CreateGraphSchemaEdgeKind - creates a new row in the schema_edge_kinds table. A model.GraphSchemaEdgeKind struct is +// returned, populated with the value as it stands in the database. This will also create a kind in the DAWGS kind table +// // if the kind does not already exist. Since this inserts directly into the kinds table, the business logic calling this func +// // must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind + // DO UPDATE forces the CTE to return the id and name, nothing would be returned if DO NOTHING is used if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` - INSERT INTO %s (name, schema_extension_id, description, is_traversable) - VALUES (?, ?, ?, ?) - RETURNING id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at`, schemaEdgeKind.TableName()), - name, schemaExtensionId, description, isTraversable).Scan(&schemaEdgeKind); result.Error != nil { + WITH dawgs_kinds AS ( + INSERT INTO %s (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? + RETURNING id, name + ) + INSERT INTO %s (id, name, schema_extension_id, description, is_traversable) + SELECT id, name, ?, ?, ? + FROM dawgs_kinds + RETURNING id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at;`, kindTable, + schemaEdgeKind.TableName()), name, 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) } @@ -454,6 +478,10 @@ func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name strin return schemaEdgeKind, nil } +// TODO: Node/Edge kinds cannot update name without updating their DAWGS kind name. + +// GetGraphSchemaEdgeKinds - returns all rows from the schema_edge_kinds table that matches the given model.Filters. It returns a slice of model.GraphSchemaEdgeKinds +// populated with data, as well as an integer indicating the total number of rows returned by the query (excluding any given pagination). func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaEdgeKinds, int, error) { var ( schemaEdgeKinds = model.GraphSchemaEdgeKinds{} @@ -519,14 +547,17 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKindById(ctx context.Context, schemaEdg FROM %s WHERE id = ?`, schemaEdgeKind.TableName()), schemaEdgeKindId).First(&schemaEdgeKind)) } -// UpdateGraphSchemaEdgeKind - updates a row in the schema_edge_kinds table based on the provided id. It will return an error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. +// UpdateGraphSchemaEdgeKind - updates a row in the schema_edge_kinds table based on the provided id. It will return an +// error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. +// A model.GraphSchemaEdgeKind cannot have its name updated due to FK'ing to the DAWGS kind table. A new edge kind should +// be created if the names differ. func (s *BloodhoundDB) UpdateGraphSchemaEdgeKind(ctx context.Context, schemaEdgeKind model.GraphSchemaEdgeKind) (model.GraphSchemaEdgeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` UPDATE %s - SET name = ?, schema_extension_id = ?, description = ?, is_traversable = ?, updated_at = NOW() + SET schema_extension_id = ?, description = ?, is_traversable = ?, updated_at = NOW() WHERE id = ? RETURNING id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at`, schemaEdgeKind.TableName()), - schemaEdgeKind.Name, schemaEdgeKind.SchemaExtensionId, schemaEdgeKind.Description, schemaEdgeKind.IsTraversable, + schemaEdgeKind.SchemaExtensionId, schemaEdgeKind.Description, schemaEdgeKind.IsTraversable, schemaEdgeKind.ID).Scan(&schemaEdgeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { return schemaEdgeKind, fmt.Errorf("%w: %v", ErrDuplicateSchemaEdgeKindName, result.Error) diff --git a/cmd/api/src/database/graphschema_integration_test.go b/cmd/api/src/database/graphschema_integration_test.go index f7b6dc57642..af8066d4039 100644 --- a/cmd/api/src/database/graphschema_integration_test.go +++ b/cmd/api/src/database/graphschema_integration_test.go @@ -461,17 +461,26 @@ func TestDatabase_GraphSchemaNodeKind_CRUD(t *testing.T) { // UPDATE - // Expected success - update schema node kind 1 to want 3 + // Expected success - update schema node kind 1 to want 3, the name should NOT be updated t.Run("success - update schema node kind 1 to want 3", func(t *testing.T) { updateWant.ID = gotNodeKind1.ID gotUpdateNodeKind3, err := testSuite.BHDatabase.UpdateGraphSchemaNodeKind(testSuite.Context, updateWant) require.NoError(t, err) - compareGraphSchemaNodeKind(t, gotUpdateNodeKind3, updateWant) - }) - // Expected fail - return an error if update violates table constraints (updating the first kind to match the second) - t.Run("fail - update schema node kind does not have unique name", func(t *testing.T) { - _, err = testSuite.BHDatabase.UpdateGraphSchemaNodeKind(testSuite.Context, model.GraphSchemaNodeKind{Serial: model.Serial{ID: gotNodeKind1.ID}, Name: "Test_Kind_2", SchemaExtensionId: extension.ID}) - require.ErrorIs(t, err, database.ErrDuplicateSchemaNodeKindName) + compareGraphSchemaNodeKind(t, gotUpdateNodeKind3, model.GraphSchemaNodeKind{ + Serial: model.Serial{ + Basic: model.Basic{ + CreatedAt: updateWant.CreatedAt, + UpdatedAt: updateWant.UpdatedAt, + }, + }, + Name: nodeKind1.Name, + SchemaExtensionId: updateWant.SchemaExtensionId, + DisplayName: updateWant.DisplayName, + Description: updateWant.Description, + IsDisplayKind: updateWant.IsDisplayKind, + Icon: updateWant.Icon, + IconColor: updateWant.IconColor, + }) }) // Expected fail - return an error if trying to update a node_kind that does not exist t.Run("fail - update a node kind that does not exist", func(t *testing.T) { @@ -890,17 +899,23 @@ func TestDatabase_GraphSchemaEdgeKind_CRUD(t *testing.T) { // UPDATE - // Expected success - update edgeKind1 to updateWant + // Expected success - update edgeKind1 to updateWant, the name should NOT be updated t.Run("success - update edgeKind1 to updateWant", func(t *testing.T) { updateWant.ID = gotEdgeKind1.ID gotEdgeKind3, err := testSuite.BHDatabase.UpdateGraphSchemaEdgeKind(testSuite.Context, updateWant) require.NoError(t, err) - compareGraphSchemaEdgeKind(t, gotEdgeKind3, updateWant) - }) - // Expected fail - return an error if update violates table constraints (update first edge kind to match the second) - t.Run("fail - update schema edge kind does not have a unique name", func(t *testing.T) { - _, err = testSuite.BHDatabase.UpdateGraphSchemaEdgeKind(testSuite.Context, model.GraphSchemaEdgeKind{Serial: model.Serial{ID: gotEdgeKind1.ID}, Name: edgeKind2.Name, SchemaExtensionId: extension.ID}) - require.ErrorIs(t, err, database.ErrDuplicateSchemaEdgeKindName) + compareGraphSchemaEdgeKind(t, gotEdgeKind3, model.GraphSchemaEdgeKind{ + Serial: model.Serial{ + Basic: model.Basic{ + CreatedAt: updateWant.CreatedAt, + UpdatedAt: updateWant.UpdatedAt, + }, + }, + SchemaExtensionId: updateWant.SchemaExtensionId, + Name: edgeKind1.Name, + Description: updateWant.Description, + IsTraversable: updateWant.IsTraversable, + }) }) // Expected fail - return an error if trying to update an edge_kind that does not exist t.Run("fail - update an edge kind that does not exist", func(t *testing.T) { diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index 4465379f468..db21a7fe8d8 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -25,6 +25,17 @@ VALUES (current_timestamp, false) ON CONFLICT DO NOTHING; +-- OpenGraph Schema Extension Management feature flag +INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) +VALUES (current_timestamp, + current_timestamp, + 'opengraph_extension_management', + 'OpenGraph Schema Extension Management', + 'Enable OpenGraph Schema Extension Management', + false, + false) +ON CONFLICT DO NOTHING; + -- OpenGraph graph schema - extensions (collectors) CREATE TABLE IF NOT EXISTS schema_extensions ( @@ -41,9 +52,9 @@ CREATE TABLE IF NOT EXISTS schema_extensions ( -- OpenGraph schema_node_kinds - stores node kinds for open graph extensions CREATE TABLE IF NOT EXISTS schema_node_kinds ( - id SERIAL PRIMARY KEY , + id SERIAL PRIMARY KEY REFERENCES kind (id) ON DELETE CASCADE , schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this node kind belongs to - name TEXT UNIQUE NOT NULL, -- unique is required by the DAWGS kind table + name VARCHAR(256) UNIQUE NOT NULL REFERENCES kind (name) ON DELETE CASCADE , -- unique is required by the DAWGS kind table display_name TEXT NOT NULL, -- can be different from name but usually isn't other than Base/Entity description TEXT NOT NULL, -- human-readable description of the kind is_display_kind BOOL NOT NULL DEFAULT FALSE, @@ -75,9 +86,9 @@ CREATE INDEX IF NOT EXISTS idx_schema_properties_schema_extensions_id on schema_ -- OpenGraph schema_edge_kinds - store edge kinds for open graph extensions CREATE TABLE IF NOT EXISTS schema_edge_kinds ( - id SERIAL NOT NULL, + id SERIAL NOT NULL REFERENCES kind (id) ON DELETE CASCADE, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this edge kind belongs to - name TEXT UNIQUE NOT NULL, -- unique is required by the DAWGS kind table, cypher only allows alphanumeric characters and underscores + name VARCHAR(256) UNIQUE NOT NULL REFERENCES kind (name) ON DELETE CASCADE, -- unique is required by the DAWGS kind table, cypher only allows alphanumeric characters and underscores description TEXT NOT NULL, -- human-readable description of the edge-kind is_traversable BOOL NOT NULL DEFAULT FALSE, -- indicates whether the given edge-kind is traversable created_at TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp, From fac4895b202da918eb4e84fcfd76852e1dc299ef Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:12:44 -0700 Subject: [PATCH 04/27] update doc strings --- cmd/api/src/database/graphschema.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 69c709c352f..7ec59ef40a4 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -454,8 +454,8 @@ func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID // CreateGraphSchemaEdgeKind - creates a new row in the schema_edge_kinds table. A model.GraphSchemaEdgeKind struct is // returned, populated with the value as it stands in the database. This will also create a kind in the DAWGS kind table -// // if the kind does not already exist. Since this inserts directly into the kinds table, the business logic calling this func -// // must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. +// if the kind does not already exist. Since this inserts directly into the kinds table, the business logic calling this func +// must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind @@ -478,8 +478,6 @@ func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name strin return schemaEdgeKind, nil } -// TODO: Node/Edge kinds cannot update name without updating their DAWGS kind name. - // GetGraphSchemaEdgeKinds - returns all rows from the schema_edge_kinds table that matches the given model.Filters. It returns a slice of model.GraphSchemaEdgeKinds // populated with data, as well as an integer indicating the total number of rows returned by the query (excluding any given pagination). func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaEdgeKinds, int, error) { From 66e7063efc71661b2ec68e94131896f612a90839 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:19:51 -0700 Subject: [PATCH 05/27] update doc strings --- cmd/api/src/database/graphschema.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 7ec59ef40a4..1eb09acaa6c 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -419,7 +419,7 @@ func (s *BloodhoundDB) GetGraphSchemaPropertyById(ctx context.Context, extension return extensionProperty, nil } -// UpdateGraphSchemaProperty - updates a row in the schema_property table based on the provided id. It will return an error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. +// UpdateGraphSchemaProperty - updates a row in the schema_properties table based on the provided id. It will return an error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. 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 = ?, schema_extension_id = ?, display_name = ?, data_type = ?, description = ?, updated_at = NOW() WHERE id = ? @@ -437,7 +437,7 @@ func (s *BloodhoundDB) UpdateGraphSchemaProperty(ctx context.Context, property m return property, nil } -// DeleteGraphSchemaProperty - deletes a schema_property row based on the provided id. It will return an error if that id does not exist. +// DeleteGraphSchemaProperty - deletes a schema_properties row based on the provided id. It will return an error if that id does not exist. func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID int32) error { var property model.GraphSchemaProperty From b6bdad85d506000dcd1e273f15058437d0ba96f2 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:05:01 -0700 Subject: [PATCH 06/27] update id to small int and update table doc strings --- cmd/api/src/database/migration/migrations/v8.5.0.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index db21a7fe8d8..2601873c467 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -50,9 +50,9 @@ CREATE TABLE IF NOT EXISTS schema_extensions ( PRIMARY KEY (id) ); --- OpenGraph schema_node_kinds - stores node kinds for open graph extensions +-- OpenGraph schema_node_kinds - stores node kinds for open graph extensions. This FK's to the DAWGS kind table directly. CREATE TABLE IF NOT EXISTS schema_node_kinds ( - id SERIAL PRIMARY KEY REFERENCES kind (id) ON DELETE CASCADE , + id SMALLINT PRIMARY KEY REFERENCES kind (id) ON DELETE CASCADE, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this node kind belongs to name VARCHAR(256) UNIQUE NOT NULL REFERENCES kind (name) ON DELETE CASCADE , -- unique is required by the DAWGS kind table display_name TEXT NOT NULL, -- can be different from name but usually isn't other than Base/Entity @@ -84,9 +84,9 @@ CREATE TABLE IF NOT EXISTS schema_properties ( CREATE INDEX IF NOT EXISTS idx_schema_properties_schema_extensions_id on schema_properties (schema_extension_id); --- OpenGraph schema_edge_kinds - store edge kinds for open graph extensions +-- OpenGraph schema_edge_kinds - store edge kinds for open graph extensions. This FK's to the DAWGS kind table directly. CREATE TABLE IF NOT EXISTS schema_edge_kinds ( - id SERIAL NOT NULL REFERENCES kind (id) ON DELETE CASCADE, + id SMALLINT NOT NULL REFERENCES kind (id) ON DELETE CASCADE, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this edge kind belongs to name VARCHAR(256) UNIQUE NOT NULL REFERENCES kind (name) ON DELETE CASCADE, -- unique is required by the DAWGS kind table, cypher only allows alphanumeric characters and underscores description TEXT NOT NULL, -- human-readable description of the edge-kind From 33f0fc7cb50bc12a232e9d38db48bbbc29bdf6d7 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:05:41 -0700 Subject: [PATCH 07/27] remove feature flag --- cmd/api/src/database/migration/migrations/v8.5.0.sql | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index 2601873c467..4b4566ef8ac 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -25,18 +25,6 @@ VALUES (current_timestamp, false) ON CONFLICT DO NOTHING; --- OpenGraph Schema Extension Management feature flag -INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable) -VALUES (current_timestamp, - current_timestamp, - 'opengraph_extension_management', - 'OpenGraph Schema Extension Management', - 'Enable OpenGraph Schema Extension Management', - false, - false) -ON CONFLICT DO NOTHING; - - -- OpenGraph graph schema - extensions (collectors) CREATE TABLE IF NOT EXISTS schema_extensions ( id SERIAL NOT NULL, From caa0289d14836b83a4eddd7d90dee0187c7f692e Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:23:39 -0700 Subject: [PATCH 08/27] clarify doc strings --- cmd/api/src/database/graphschema.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 1eb09acaa6c..3e24b7e0ba6 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -202,12 +202,15 @@ func (s *BloodhoundDB) DeleteGraphSchemaExtension(ctx context.Context, extension // CreateGraphSchemaNodeKind - creates a new row in the schema_node_kinds table. A model.GraphSchemaNodeKind struct is // returned, populated with the value as it stands in the database. This will also create a kind in the DAWGS kind table -// if the kind does not already exist. Since this inserts directly into the kinds table, the business logic calling this func +// if the kind does not already exist. +// +// Since this inserts directly into the kinds table, the business logic calling this func // must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaNodeKind(ctx context.Context, name string, extensionId int32, displayName string, description string, isDisplayKind bool, icon, iconColor string) (model.GraphSchemaNodeKind, error) { schemaNodeKind := model.GraphSchemaNodeKind{} - // DO UPDATE forces the CTE to return the id and name, nothing would be returned if DO NOTHING is used + // DO UPDATE forces the CTE to return the id and name, DO NOTHING returns an empty id and name, breaking + // the schema table insert if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` WITH dawgs_kinds AS ( INSERT INTO %s (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? @@ -295,8 +298,9 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKindById(ctx context.Context, schemaNod // UpdateGraphSchemaNodeKind - 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. -// A model.GraphSchemaNodeKind cannot have its name updated due to FK'ing to the DAWGS kind table. A new node kind should -// be created if the names differ. +// +// This function does NOT update the name column since the schema_node_kinds table FKs to the DAWGS kind table, and that +// table is append only. A new node kind should be created instead. func (s *BloodhoundDB) UpdateGraphSchemaNodeKind(ctx context.Context, schemaNodeKind model.GraphSchemaNodeKind) (model.GraphSchemaNodeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` UPDATE %s @@ -454,12 +458,15 @@ func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID // CreateGraphSchemaEdgeKind - creates a new row in the schema_edge_kinds table. A model.GraphSchemaEdgeKind struct is // returned, populated with the value as it stands in the database. This will also create a kind in the DAWGS kind table -// if the kind does not already exist. Since this inserts directly into the kinds table, the business logic calling this func +// if the kind does not already exist. +// +// Since this inserts directly into the kinds table, the business logic calling this func // must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind - // DO UPDATE forces the CTE to return the id and name, nothing would be returned if DO NOTHING is used + // DO UPDATE forces the CTE to return the id and name, DO NOTHING returns an empty id and name, breaking + // the schema table insert if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` WITH dawgs_kinds AS ( INSERT INTO %s (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? @@ -547,8 +554,9 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKindById(ctx context.Context, schemaEdg // UpdateGraphSchemaEdgeKind - updates a row in the schema_edge_kinds table based on the provided id. It will return an // error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. -// A model.GraphSchemaEdgeKind cannot have its name updated due to FK'ing to the DAWGS kind table. A new edge kind should -// be created if the names differ. +// +// This function does NOT update the name column since the schema_edge_kinds table FKs to the DAWGS kind table, and that +// table is append only. A new edge kind should be created instead. func (s *BloodhoundDB) UpdateGraphSchemaEdgeKind(ctx context.Context, schemaEdgeKind model.GraphSchemaEdgeKind) (model.GraphSchemaEdgeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` UPDATE %s From 7c6b1649f7d90efc9d8a3d1bddd60aa659828191 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:40:45 -0500 Subject: [PATCH 09/27] BED-6721: run data population --- cmd/api/src/bootstrap/server.go | 8 ++++ .../changelog/ingestion_integration_test.go | 1 + .../datapipe/datapipe_integration_test.go | 3 ++ .../src/database/database_integration_test.go | 3 ++ cmd/api/src/database/db.go | 13 +++++- cmd/api/src/database/migration/migration.go | 11 ++++- cmd/api/src/database/migration/stepwise.go | 46 ++++++++++++++++++- cmd/api/src/database/mocks/db.go | 14 ++++++ cmd/api/src/services/entrypoint.go | 2 + .../graphify/graphify_integration_test.go | 3 ++ cmd/api/src/test/integration/database.go | 2 + cmd/api/src/test/lab/fixtures/postgres.go | 2 + packages/go/graphify/graph/graph.go | 2 + 13 files changed, 106 insertions(+), 4 deletions(-) diff --git a/cmd/api/src/bootstrap/server.go b/cmd/api/src/bootstrap/server.go index 2fe48d89d16..985ff597a52 100644 --- a/cmd/api/src/bootstrap/server.go +++ b/cmd/api/src/bootstrap/server.go @@ -72,6 +72,14 @@ func MigrateDB(ctx context.Context, cfg config.Configuration, db database.Databa return CreateDefaultAdmin(ctx, cfg, db, defaultAdminFunc) } +func PopulateExtensionData(ctx context.Context, db database.Database) error { + if err := db.PopulateExtensionData(ctx); err != nil { + return err + } + + return nil +} + func CreateDefaultAdmin(ctx context.Context, cfg config.Configuration, db database.Database, defaultAdminFunction func() (config.DefaultAdminConfiguration, error)) error { var secretDigester = cfg.Crypto.Argon2.NewDigester() diff --git a/cmd/api/src/daemons/changelog/ingestion_integration_test.go b/cmd/api/src/daemons/changelog/ingestion_integration_test.go index 98a697c3ada..dd8067cc712 100644 --- a/cmd/api/src/daemons/changelog/ingestion_integration_test.go +++ b/cmd/api/src/daemons/changelog/ingestion_integration_test.go @@ -116,6 +116,7 @@ func setupIntegrationTest(t *testing.T) IntegrationTestSuite { db := database.NewBloodhoundDB(gormDB, auth.NewIdentityResolver()) require.NoError(t, db.Migrate(ctx)) + require.NoError(t, db.PopulateExtensionData(ctx)) return IntegrationTestSuite{ Context: ctx, diff --git a/cmd/api/src/daemons/datapipe/datapipe_integration_test.go b/cmd/api/src/daemons/datapipe/datapipe_integration_test.go index 6a1fd923d47..83de9010eef 100644 --- a/cmd/api/src/daemons/datapipe/datapipe_integration_test.go +++ b/cmd/api/src/daemons/datapipe/datapipe_integration_test.go @@ -90,6 +90,9 @@ func setupIntegrationTestSuite(t *testing.T, fixturesPath string) IntegrationTes err = db.Migrate(ctx) require.NoError(t, err) + err = db.PopulateExtensionData(ctx) + require.NoError(t, err) + err = graphDB.AssertSchema(ctx, graphschema.DefaultGraphSchema()) require.NoError(t, err) diff --git a/cmd/api/src/database/database_integration_test.go b/cmd/api/src/database/database_integration_test.go index 2b31c272017..77b5615c59e 100644 --- a/cmd/api/src/database/database_integration_test.go +++ b/cmd/api/src/database/database_integration_test.go @@ -58,6 +58,9 @@ func setupIntegrationTestSuite(t *testing.T) IntegrationTestSuite { err = db.Migrate(ctx) require.NoError(t, err) + err = db.PopulateExtensionData(ctx) + require.NoError(t, err) + // #endregion return IntegrationTestSuite{ diff --git a/cmd/api/src/database/db.go b/cmd/api/src/database/db.go index 6343f788d76..b1d18672ae3 100644 --- a/cmd/api/src/database/db.go +++ b/cmd/api/src/database/db.go @@ -33,6 +33,7 @@ import ( "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" + "github.com/specterops/bloodhound/packages/go/bhlog/attr" "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -97,6 +98,7 @@ type Database interface { Wipe(ctx context.Context) error Migrate(ctx context.Context) error + PopulateExtensionData(ctx context.Context) error CreateInstallation(ctx context.Context) (model.Installation, error) GetInstallation(ctx context.Context) (model.Installation, error) HasInstallation(ctx context.Context) (bool, error) @@ -266,7 +268,16 @@ func (s *BloodhoundDB) Wipe(ctx context.Context) error { func (s *BloodhoundDB) Migrate(ctx context.Context) error { // Run the migrator if err := migration.NewMigrator(s.db.WithContext(ctx)).ExecuteStepwiseMigrations(); err != nil { - slog.ErrorContext(ctx, fmt.Sprintf("Error during SQL database migration phase: %v", err)) + slog.ErrorContext(ctx, "Error during SQL database migration phase", attr.Error(err)) + return err + } + + return nil +} + +func (s *BloodhoundDB) PopulateExtensionData(ctx context.Context) error { + if err := migration.NewMigrator(s.db.WithContext(ctx)).ExecuteExtensionDataPopulation(); err != nil { + slog.ErrorContext(ctx, "Error during extensions data population phase", attr.Error(err)) return err } diff --git a/cmd/api/src/database/migration/migration.go b/cmd/api/src/database/migration/migration.go index e11fcbdd828..fc29588a8fd 100644 --- a/cmd/api/src/database/migration/migration.go +++ b/cmd/api/src/database/migration/migration.go @@ -27,6 +27,9 @@ import ( //go:embed migrations var FossMigrations embed.FS +//go:embed extensions +var ExtensionMigrations embed.FS + // Source is meant to be a file system source that contains SQL migration files. type Source struct { FileSystem fs.FS @@ -42,8 +45,9 @@ type Migration struct { // Migrator is the main SQL migration tool for BloodHound. type Migrator struct { - Sources []Source - DB *gorm.DB + Sources []Source + ExtensionsData []Source + DB *gorm.DB } // NewMigrator returns a new Migrator with the FossMigrations Source predefined. @@ -52,6 +56,9 @@ func NewMigrator(db *gorm.DB) *Migrator { Sources: []Source{ {FileSystem: FossMigrations, Directory: "migrations"}, }, + ExtensionsData: []Source{ + {FileSystem: ExtensionMigrations, Directory: "extensions"}, + }, DB: db, } } diff --git a/cmd/api/src/database/migration/stepwise.go b/cmd/api/src/database/migration/stepwise.go index 9e17b6c123d..78ba63097ff 100644 --- a/cmd/api/src/database/migration/stepwise.go +++ b/cmd/api/src/database/migration/stepwise.go @@ -20,6 +20,8 @@ import ( "fmt" "io/fs" "log/slog" + "path/filepath" + "strings" "github.com/specterops/bloodhound/cmd/api/src/model" "github.com/specterops/bloodhound/cmd/api/src/version" @@ -49,7 +51,7 @@ func (s *Migrator) ExecuteMigrations(manifest Manifest) error { } // execute the migration(s) for this version in a transaction - slog.Info(fmt.Sprintf("Executing SQL migrations for %s", versionString)) + slog.Info("Executing SQL migrations", slog.String("version", versionString)) if err := s.DB.Transaction(func(tx *gorm.DB) error { for _, migration := range manifest.Migrations[versionString] { @@ -193,3 +195,45 @@ func (s *Migrator) ExecuteStepwiseMigrations() error { return nil } } + +func (s *Migrator) ExecuteExtensionDataPopulation() error { + const migrationSQLFilenameSuffix = ".sql" + + // loop through extensions data + for _, source := range s.ExtensionsData { + dirEntries, err := fs.ReadDir(source.FileSystem, source.Directory) + if err != nil { + return err + } + + // loop through file system entries + for _, entry := range dirEntries { + if entry.IsDir() { + continue + } + + filename := filepath.Join(source.Directory, entry.Name()) + basename := filepath.Base(filename) + + if !strings.HasSuffix(basename, migrationSQLFilenameSuffix) { + continue + } + + slog.Info("Executing extension data population", slog.String("file", basename)) + if err := s.DB.Transaction(func(tx *gorm.DB) error { + // read migration file content and execute + if migrationContent, err := fs.ReadFile(source.FileSystem, filename); err != nil { + return err + } else if result := tx.Exec(string(migrationContent)); result.Error != nil { + return result.Error + } + + return nil + }); err != nil { + return fmt.Errorf("failed to execute extension data population for %s: %w", basename, err) + } + } + } + + return nil +} diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index 7a06e543e67..29547918b29 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -2410,6 +2410,20 @@ func (mr *MockDatabaseMockRecorder) Migrate(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockDatabase)(nil).Migrate), ctx) } +// PopulateExtensionData mocks base method. +func (m *MockDatabase) PopulateExtensionData(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PopulateExtensionData", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// PopulateExtensionData indicates an expected call of PopulateExtensionData. +func (mr *MockDatabaseMockRecorder) PopulateExtensionData(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopulateExtensionData", reflect.TypeOf((*MockDatabase)(nil).PopulateExtensionData), ctx) +} + // RegisterSourceKind mocks base method. func (m *MockDatabase) RegisterSourceKind(ctx context.Context) func(graph.Kind) error { m.ctrl.T.Helper() diff --git a/cmd/api/src/services/entrypoint.go b/cmd/api/src/services/entrypoint.go index 10ff850660a..1fa9937ab58 100644 --- a/cmd/api/src/services/entrypoint.go +++ b/cmd/api/src/services/entrypoint.go @@ -80,6 +80,8 @@ func Entrypoint(ctx context.Context, cfg config.Configuration, connections boots if !cfg.DisableMigrations { if err := bootstrap.MigrateDB(ctx, cfg, connections.RDMS, config.NewDefaultAdminConfiguration); err != nil { return nil, fmt.Errorf("rdms migration error: %w", err) + } else if err := bootstrap.PopulateExtensionData(ctx, connections.RDMS); err != nil { + return nil, fmt.Errorf("extensions data population error: %w", err) } else if err := migrations.NewGraphMigrator(connections.Graph).Migrate(ctx); err != nil { return nil, fmt.Errorf("graph migration error: %w", err) } diff --git a/cmd/api/src/services/graphify/graphify_integration_test.go b/cmd/api/src/services/graphify/graphify_integration_test.go index 8dfd4ddd945..989155bbeb7 100644 --- a/cmd/api/src/services/graphify/graphify_integration_test.go +++ b/cmd/api/src/services/graphify/graphify_integration_test.go @@ -85,6 +85,9 @@ func setupIntegrationTestSuite(t *testing.T, fixturesPath string) IntegrationTes err = db.Migrate(ctx) require.NoError(t, err) + err = db.PopulateExtensionData(ctx) + require.NoError(t, err) + err = graphDB.AssertSchema(ctx, graphschema.DefaultGraphSchema()) require.NoError(t, err) diff --git a/cmd/api/src/test/integration/database.go b/cmd/api/src/test/integration/database.go index 53ef0b7ae79..b76417f56fd 100644 --- a/cmd/api/src/test/integration/database.go +++ b/cmd/api/src/test/integration/database.go @@ -136,6 +136,8 @@ func Prepare(ctx context.Context, db database.Database) error { return fmt.Errorf("failed to clear database: %v", err) } else if err := db.Migrate(ctx); err != nil { return fmt.Errorf("failed to migrate database: %v", err) + } else if err := db.PopulateExtensionData(ctx); err != nil { + return fmt.Errorf("failed to populate extension data: %v", err) } return nil diff --git a/cmd/api/src/test/lab/fixtures/postgres.go b/cmd/api/src/test/lab/fixtures/postgres.go index f63ad1851c1..a7b0bc91277 100644 --- a/cmd/api/src/test/lab/fixtures/postgres.go +++ b/cmd/api/src/test/lab/fixtures/postgres.go @@ -41,6 +41,8 @@ var PostgresFixture = lab.NewFixture(func(harness *lab.Harness) (*database.Blood return nil, fmt.Errorf("failed ensuring database: %v", err) } else if err := bootstrap.MigrateDB(testCtx, labConfig, database.NewBloodhoundDB(pgdb, auth.NewIdentityResolver()), config.NewDefaultAdminConfiguration); err != nil { return nil, fmt.Errorf("failed migrating database: %v", err) + } else if err := bootstrap.PopulateExtensionData(testCtx, database.NewBloodhoundDB(pgdb, auth.NewIdentityResolver())); err != nil { + return nil, fmt.Errorf("failed populating extension data: %v", err) } else { return database.NewBloodhoundDB(pgdb, auth.NewIdentityResolver()), nil } diff --git a/packages/go/graphify/graph/graph.go b/packages/go/graphify/graph/graph.go index db721083f5c..87437feb911 100644 --- a/packages/go/graphify/graph/graph.go +++ b/packages/go/graphify/graph/graph.go @@ -175,6 +175,8 @@ func (s *CommunityGraphService) InitializeService(ctx context.Context, connectio if err := s.db.Migrate(ctx); err != nil { return fmt.Errorf("error migrating database: %w", err) + } else if err := s.db.PopulateExtensionData(ctx); err != nil { + return fmt.Errorf("error populating extension data: %w", err) } else if err := migrations.NewGraphMigrator(graphDB).Migrate(ctx); err != nil { return fmt.Errorf("error migrating graph schema: %w", err) } else if err = graphDB.SetDefaultGraph(ctx, graphschema.DefaultGraph()); err != nil { From a7e1e2ac3838bb4b386a6628d9f3efd66410c404 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:07:13 -0500 Subject: [PATCH 10/27] BED-6721: abstract function and rename files --- LICENSE.header | 2 +- .../{ad.sql => ad_graph_schema.sql} | 2 +- .../{az.sql => az_graph_schema.sql} | 2 +- packages/csharp/graphschema/PropertyNames.cs | 2 +- packages/go/graphschema/ad/ad.go | 2 +- packages/go/graphschema/azure/azure.go | 2 +- packages/go/graphschema/common/common.go | 2 +- packages/go/graphschema/graph.go | 2 +- packages/go/schemagen/generator/sql.go | 91 ++++--------------- packages/go/schemagen/main.go | 11 +-- .../bh-shared-ui/src/graphSchema.ts | 2 +- 11 files changed, 30 insertions(+), 90 deletions(-) rename cmd/api/src/database/migration/extensions/{ad.sql => ad_graph_schema.sql} (99%) rename cmd/api/src/database/migration/extensions/{az.sql => az_graph_schema.sql} (99%) diff --git a/LICENSE.header b/LICENSE.header index 5d5c596b1c7..958be83318a 100644 --- a/LICENSE.header +++ b/LICENSE.header @@ -1,4 +1,4 @@ -Copyright 2025 Specter Ops, Inc. +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. diff --git a/cmd/api/src/database/migration/extensions/ad.sql b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql similarity index 99% rename from cmd/api/src/database/migration/extensions/ad.sql rename to cmd/api/src/database/migration/extensions/ad_graph_schema.sql index 443a3748d84..665747958f5 100644 --- a/cmd/api/src/database/migration/extensions/ad.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -1,4 +1,4 @@ --- Copyright 2025 Specter Ops, Inc. +-- 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. diff --git a/cmd/api/src/database/migration/extensions/az.sql b/cmd/api/src/database/migration/extensions/az_graph_schema.sql similarity index 99% rename from cmd/api/src/database/migration/extensions/az.sql rename to cmd/api/src/database/migration/extensions/az_graph_schema.sql index 1e2c62065ac..38de5ff15f1 100644 --- a/cmd/api/src/database/migration/extensions/az.sql +++ b/cmd/api/src/database/migration/extensions/az_graph_schema.sql @@ -1,4 +1,4 @@ --- Copyright 2025 Specter Ops, Inc. +-- 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. diff --git a/packages/csharp/graphschema/PropertyNames.cs b/packages/csharp/graphschema/PropertyNames.cs index fc3d6b08a4f..2031b9df97c 100644 --- a/packages/csharp/graphschema/PropertyNames.cs +++ b/packages/csharp/graphschema/PropertyNames.cs @@ -1,5 +1,5 @@ /* - Copyright 2025 Specter Ops, Inc. + 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. diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 4e21691829c..55393a86b0e 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/azure/azure.go b/packages/go/graphschema/azure/azure.go index 3ea14839490..709eab6828b 100644 --- a/packages/go/graphschema/azure/azure.go +++ b/packages/go/graphschema/azure/azure.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index f18f99a486b..057e552ab76 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/graph.go b/packages/go/graphschema/graph.go index aedef13acd5..acb1385859e 100644 --- a/packages/go/graphschema/graph.go +++ b/packages/go/graphschema/graph.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 79a285e0307..3df48577fd9 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -24,12 +24,12 @@ import ( "github.com/specterops/bloodhound/packages/go/schemagen/model" ) -type NodeIcon struct { +type nodeIcon struct { Icon string Color string } -var NodeIcons = map[string]NodeIcon{ +var nodeIcons = map[string]nodeIcon{ // Active Directory Node Types "User": { Icon: "fa-user", @@ -163,93 +163,34 @@ var NodeIcons = map[string]NodeIcon{ } func GenerateExtensionSQLActiveDirectory(dir string, adSchema model.ActiveDirectory) error { - var sb strings.Builder - - sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/\n", SchemaSourceName)) - - sb.WriteString("DELETE FROM schema_extensions WHERE name = 'AD';\n\n") - - sb.WriteString("DO $$\nDECLARE\n\tnew_extension_id INT;\nBEGIN\n") - - sb.WriteString("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true) RETURNING id INTO new_extension_id;\n\n") - - sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") - - for i, kind := range adSchema.NodeKinds { - if iconInfo, found := NodeIcons[kind.Symbol]; found { - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) - } else { - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) - } - - if i != len(adSchema.NodeKinds)-1 { - sb.WriteString(",\n") - } - } - - sb.WriteString(";\n\n") - - sb.WriteString("\tINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") - - traversableMap := make(map[string]struct{}) - - for _, kind := range adSchema.PathfindingRelationships { - traversableMap[kind.Symbol] = struct{}{} - } - - for i, kind := range adSchema.RelationshipKinds { - _, traversable := traversableMap[kind.Symbol] - - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '', %t)", kind.GetRepresentation(), traversable)) - - if i != len(adSchema.RelationshipKinds)-1 { - sb.WriteString(",\n") - } - } - - sb.WriteString(";\nEND $$;") - - if _, err := os.Stat(dir); err != nil { - if !os.IsNotExist(err) { - return err - } - - if err := os.MkdirAll(dir, defaultPackageDirPermission); err != nil { - return err - } - } - - if fout, err := os.OpenFile(path.Join(dir, "ad.sql"), fileOpenMode, defaultSourceFilePermission); err != nil { - return err - } else { - defer fout.Close() - - _, err := fout.WriteString(sb.String()) - return err - } + return GenerateExtensionSQL("AD", "Active Directory", "v0.0.1", dir, "ad_graph_schema.sql", adSchema.NodeKinds, adSchema.RelationshipKinds, adSchema.PathfindingRelationships) } func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { + return GenerateExtensionSQL("AZ", "Azure", "v0.0.1", dir, "az_graph_schema.sql", azSchema.NodeKinds, azSchema.RelationshipKinds, azSchema.PathfindingRelationships) +} + +func GenerateExtensionSQL(name string, displayName string, version string, dir string, fileName string, nodeKinds []model.StringEnum, relationshipKinds []model.StringEnum, pathfindingRelationshipKinds []model.StringEnum) error { var sb strings.Builder sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/\n", SchemaSourceName)) - sb.WriteString("DELETE FROM schema_extensions WHERE name = 'AZ';\n\n") + sb.WriteString(fmt.Sprintf("DELETE FROM schema_extensions WHERE name = '%s';\n\n", name)) sb.WriteString("DO $$\nDECLARE\n\tnew_extension_id INT;\nBEGIN\n") - sb.WriteString("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true) RETURNING id INTO new_extension_id;\n\n") + sb.WriteString(fmt.Sprintf("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('%s', '%s', '%s', true) RETURNING id INTO new_extension_id;\n\n", name, displayName, version)) sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") - for i, kind := range azSchema.NodeKinds { - if iconInfo, found := NodeIcons[kind.Symbol]; found { + for i, kind := range nodeKinds { + if iconInfo, found := nodeIcons[kind.Symbol]; found { sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) } else { sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) } - if i != len(azSchema.NodeKinds)-1 { + if i != len(nodeKinds)-1 { sb.WriteString(",\n") } } @@ -260,16 +201,16 @@ func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { traversableMap := make(map[string]struct{}) - for _, kind := range azSchema.PathfindingRelationships { + for _, kind := range pathfindingRelationshipKinds { traversableMap[kind.Symbol] = struct{}{} } - for i, kind := range azSchema.RelationshipKinds { + for i, kind := range relationshipKinds { _, traversable := traversableMap[kind.Symbol] sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '', %t)", kind.GetRepresentation(), traversable)) - if i != len(azSchema.RelationshipKinds)-1 { + if i != len(relationshipKinds)-1 { sb.WriteString(",\n") } } @@ -286,7 +227,7 @@ func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { } } - if fout, err := os.OpenFile(path.Join(dir, "az.sql"), fileOpenMode, defaultSourceFilePermission); err != nil { + if fout, err := os.OpenFile(path.Join(dir, fileName), fileOpenMode, defaultSourceFilePermission); err != nil { return err } else { defer fout.Close() diff --git a/packages/go/schemagen/main.go b/packages/go/schemagen/main.go index df037cc2d5b..8628e1fc4f0 100644 --- a/packages/go/schemagen/main.go +++ b/packages/go/schemagen/main.go @@ -17,7 +17,6 @@ package main import ( - "fmt" "log/slog" "os" "path/filepath" @@ -108,27 +107,27 @@ func main() { var bhModels Schema if err := bhInstance.Decode(&bhModels); err != nil { - slog.Error(fmt.Sprintf("Error: %v", errors.Details(err, nil))) + slog.Error("Failed to decode models", slog.String("err", errors.Details(err, nil))) os.Exit(1) } if err := GenerateGolang(projectRoot, bhModels); err != nil { - slog.Error(fmt.Sprintf("Error %v", err)) + slog.Error("Failed to generate Golang", attr.Error(err)) os.Exit(1) } if err := GenerateSharedTypeScript(projectRoot, bhModels); err != nil { - slog.Error(fmt.Sprintf("Error %v", err)) + slog.Error("Failed to generate TypeScript", attr.Error(err)) os.Exit(1) } if err := GenerateCSharp(projectRoot, bhModels); err != nil { - slog.Error(fmt.Sprintf("Error %v", err)) + slog.Error("Failed to generate CSharp", attr.Error(err)) os.Exit(1) } if err := GenerateSQL(projectRoot, bhModels); err != nil { - slog.Error(fmt.Sprintf("Error %v", err)) + slog.Error("Failed to generate SQL", attr.Error(err)) os.Exit(1) } } diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 7ede0cf4037..c92263f53ed 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. From 91c5b4a0660a3b955582f1e75443bd2087685b34 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:05:23 -0500 Subject: [PATCH 11/27] BED-6721: fix az schema, run just generate --- cmd/api/src/api/mocks/authenticator.go | 2 +- cmd/api/src/daemons/datapipe/mocks/cleanup.go | 2 +- .../migration/extensions/az_graph_schema.sql | 38 +++++++++---------- cmd/api/src/database/mocks/auth.go | 2 +- cmd/api/src/database/mocks/db.go | 2 +- cmd/api/src/queries/mocks/graph.go | 2 +- cmd/api/src/services/agi/mocks/mock.go | 2 +- .../src/services/dataquality/mocks/mock.go | 2 +- cmd/api/src/services/fs/mocks/fs.go | 2 +- cmd/api/src/services/graphify/mocks/ingest.go | 2 +- cmd/api/src/services/oidc/mocks/oidc.go | 2 +- cmd/api/src/services/saml/mocks/saml.go | 2 +- cmd/api/src/services/upload/mocks/mock.go | 2 +- .../src/utils/validation/mocks/validator.go | 2 +- cmd/api/src/vendormocks/dawgs/graph/mock.go | 2 +- cmd/api/src/vendormocks/io/fs/mock.go | 2 +- .../neo4j/neo4j-go-driver/v5/neo4j/mock.go | 2 +- packages/go/crypto/mocks/digest.go | 2 +- packages/go/schemagen/generator/sql.go | 6 +-- .../SelectedDetailsTabs.test.tsx | 2 +- 20 files changed, 40 insertions(+), 40 deletions(-) diff --git a/cmd/api/src/api/mocks/authenticator.go b/cmd/api/src/api/mocks/authenticator.go index 32cbd5053e8..8811703cb3e 100644 --- a/cmd/api/src/api/mocks/authenticator.go +++ b/cmd/api/src/api/mocks/authenticator.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/daemons/datapipe/mocks/cleanup.go b/cmd/api/src/daemons/datapipe/mocks/cleanup.go index ed40825455e..d2f7e31808b 100644 --- a/cmd/api/src/daemons/datapipe/mocks/cleanup.go +++ b/cmd/api/src/daemons/datapipe/mocks/cleanup.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/database/migration/extensions/az_graph_schema.sql b/cmd/api/src/database/migration/extensions/az_graph_schema.sql index 38de5ff15f1..a5854ab3adc 100644 --- a/cmd/api/src/database/migration/extensions/az_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/az_graph_schema.sql @@ -25,25 +25,25 @@ BEGIN INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES (new_extension_id, 'AZBase', 'Entity', '', false, '', ''), - (new_extension_id, 'AZVMScaleSet', 'VMScaleSet', '', false, '', ''), - (new_extension_id, 'AZApp', 'App', '', false, '', ''), - (new_extension_id, 'AZRole', 'Role', '', false, '', ''), - (new_extension_id, 'AZDevice', 'Device', '', false, '', ''), - (new_extension_id, 'AZFunctionApp', 'FunctionApp', '', false, '', ''), - (new_extension_id, 'AZGroup', 'Group', '', true, 'fa-users', '#DBE617'), - (new_extension_id, 'AZKeyVault', 'KeyVault', '', false, '', ''), - (new_extension_id, 'AZManagementGroup', 'ManagementGroup', '', false, '', ''), - (new_extension_id, 'AZResourceGroup', 'ResourceGroup', '', false, '', ''), - (new_extension_id, 'AZServicePrincipal', 'ServicePrincipal', '', false, '', ''), - (new_extension_id, 'AZSubscription', 'Subscription', '', false, '', ''), - (new_extension_id, 'AZTenant', 'Tenant', '', false, '', ''), - (new_extension_id, 'AZUser', 'User', '', true, 'fa-user', '#17E625'), - (new_extension_id, 'AZVM', 'VM', '', false, '', ''), - (new_extension_id, 'AZManagedCluster', 'ManagedCluster', '', false, '', ''), - (new_extension_id, 'AZContainerRegistry', 'ContainerRegistry', '', false, '', ''), - (new_extension_id, 'AZWebApp', 'WebApp', '', false, '', ''), - (new_extension_id, 'AZLogicApp', 'LogicApp', '', false, '', ''), - (new_extension_id, 'AZAutomationAccount', 'AutomationAccount', '', false, '', ''); + (new_extension_id, 'AZVMScaleSet', 'VMScaleSet', '', true, 'fa-server', '#007CD0'), + (new_extension_id, 'AZApp', 'App', '', true, 'fa-window-restore', '#03FC84'), + (new_extension_id, 'AZRole', 'Role', '', true, 'fa-clipboard-list', '#ED8537'), + (new_extension_id, 'AZDevice', 'Device', '', true, 'fa-desktop', '#B18FCF'), + (new_extension_id, 'AZFunctionApp', 'FunctionApp', '', true, 'fa-bolt', '#F4BA44'), + (new_extension_id, 'AZGroup', 'Group', '', true, 'fa-users', '#F57C9B'), + (new_extension_id, 'AZKeyVault', 'KeyVault', '', true, 'fa-lock', '#ED658C'), + (new_extension_id, 'AZManagementGroup', 'ManagementGroup', '', true, 'fa-sitemap', '#BD93D8'), + (new_extension_id, 'AZResourceGroup', 'ResourceGroup', '', true, 'fa-cube', '#89BD9E'), + (new_extension_id, 'AZServicePrincipal', 'ServicePrincipal', '', true, 'fa-robot', '#C1D6D6'), + (new_extension_id, 'AZSubscription', 'Subscription', '', true, 'fa-key', '#D2CCA1'), + (new_extension_id, 'AZTenant', 'Tenant', '', true, 'fa-cloud', '#54F2F2'), + (new_extension_id, 'AZUser', 'User', '', true, 'fa-user', '#34D2EB'), + (new_extension_id, 'AZVM', 'VM', '', true, 'fa-desktop', '#F9ADA0'), + (new_extension_id, 'AZManagedCluster', 'ManagedCluster', '', true, 'fa-cubes', '#326CE5'), + (new_extension_id, 'AZContainerRegistry', 'ContainerRegistry', '', true, 'fa-box-open', '#0885D7'), + (new_extension_id, 'AZWebApp', 'WebApp', '', true, 'fa-object-group', '#4696E9'), + (new_extension_id, 'AZLogicApp', 'LogicApp', '', true, 'fa-sitemap', '#9EE047'), + (new_extension_id, 'AZAutomationAccount', 'AutomationAccount', '', true, 'fa-cog', '#F4BA44'); INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES (new_extension_id, 'AZAvereContributor', '', true), diff --git a/cmd/api/src/database/mocks/auth.go b/cmd/api/src/database/mocks/auth.go index 6934b158d0e..d696b4804cc 100644 --- a/cmd/api/src/database/mocks/auth.go +++ b/cmd/api/src/database/mocks/auth.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index 35af9b7ede7..b8f7885ebfb 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/queries/mocks/graph.go b/cmd/api/src/queries/mocks/graph.go index a536fce424b..85b683c87c1 100644 --- a/cmd/api/src/queries/mocks/graph.go +++ b/cmd/api/src/queries/mocks/graph.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/agi/mocks/mock.go b/cmd/api/src/services/agi/mocks/mock.go index d6f0342f8e7..d8c4c5695b3 100644 --- a/cmd/api/src/services/agi/mocks/mock.go +++ b/cmd/api/src/services/agi/mocks/mock.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/dataquality/mocks/mock.go b/cmd/api/src/services/dataquality/mocks/mock.go index 11f0a8a31b5..0ffa179ce6e 100644 --- a/cmd/api/src/services/dataquality/mocks/mock.go +++ b/cmd/api/src/services/dataquality/mocks/mock.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/fs/mocks/fs.go b/cmd/api/src/services/fs/mocks/fs.go index 7d05415d188..5ba753d5346 100644 --- a/cmd/api/src/services/fs/mocks/fs.go +++ b/cmd/api/src/services/fs/mocks/fs.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/graphify/mocks/ingest.go b/cmd/api/src/services/graphify/mocks/ingest.go index 81b68bbc28b..c897f58c6dc 100644 --- a/cmd/api/src/services/graphify/mocks/ingest.go +++ b/cmd/api/src/services/graphify/mocks/ingest.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/oidc/mocks/oidc.go b/cmd/api/src/services/oidc/mocks/oidc.go index 704420ef381..6faa71e5d54 100644 --- a/cmd/api/src/services/oidc/mocks/oidc.go +++ b/cmd/api/src/services/oidc/mocks/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/saml/mocks/saml.go b/cmd/api/src/services/saml/mocks/saml.go index f71839355b7..e6029f56541 100644 --- a/cmd/api/src/services/saml/mocks/saml.go +++ b/cmd/api/src/services/saml/mocks/saml.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/upload/mocks/mock.go b/cmd/api/src/services/upload/mocks/mock.go index bd1cd940393..a3b90e7fb30 100644 --- a/cmd/api/src/services/upload/mocks/mock.go +++ b/cmd/api/src/services/upload/mocks/mock.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/utils/validation/mocks/validator.go b/cmd/api/src/utils/validation/mocks/validator.go index 309fe513305..536c0385a5d 100644 --- a/cmd/api/src/utils/validation/mocks/validator.go +++ b/cmd/api/src/utils/validation/mocks/validator.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/vendormocks/dawgs/graph/mock.go b/cmd/api/src/vendormocks/dawgs/graph/mock.go index 45ea5625414..14f78cb94d9 100644 --- a/cmd/api/src/vendormocks/dawgs/graph/mock.go +++ b/cmd/api/src/vendormocks/dawgs/graph/mock.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/vendormocks/io/fs/mock.go b/cmd/api/src/vendormocks/io/fs/mock.go index 5c094281bdf..f90a77b286f 100644 --- a/cmd/api/src/vendormocks/io/fs/mock.go +++ b/cmd/api/src/vendormocks/io/fs/mock.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go b/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go index a4adb00b99d..1e3a6ab7106 100644 --- a/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go +++ b/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/packages/go/crypto/mocks/digest.go b/packages/go/crypto/mocks/digest.go index 3c1acaa2cee..ddd118ab958 100644 --- a/packages/go/crypto/mocks/digest.go +++ b/packages/go/crypto/mocks/digest.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// 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. diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 3df48577fd9..a2a0b882502 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -184,7 +184,7 @@ func GenerateExtensionSQL(name string, displayName string, version string, dir s sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") for i, kind := range nodeKinds { - if iconInfo, found := nodeIcons[kind.Symbol]; found { + if iconInfo, found := nodeIcons[kind.GetRepresentation()]; found { sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) } else { sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) @@ -202,11 +202,11 @@ func GenerateExtensionSQL(name string, displayName string, version string, dir s traversableMap := make(map[string]struct{}) for _, kind := range pathfindingRelationshipKinds { - traversableMap[kind.Symbol] = struct{}{} + traversableMap[kind.GetRepresentation()] = struct{}{} } for i, kind := range relationshipKinds { - _, traversable := traversableMap[kind.Symbol] + _, traversable := traversableMap[kind.GetRepresentation()] sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '', %t)", kind.GetRepresentation(), traversable)) diff --git a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx index dab44ff51a6..e5d1bb755d0 100644 --- a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx @@ -173,4 +173,4 @@ describe('Selected Details Tabs', () => { expect(objectTab).toBeEnabled(); }); }); -}); \ No newline at end of file +}); From 4291d8fb5a39ee35d4f3bebb78e9787ab2875e8e Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:34:31 -0500 Subject: [PATCH 12/27] BED-6721: add insert statements --- .../datapipe/datapipe_integration_test.go | 4 +- .../migration/extensions/ad_graph_schema.sql | 105 ++++++++++++++++++ .../migration/extensions/az_graph_schema.sql | 72 ++++++++++++ cmd/api/src/services/entrypoint.go | 4 +- .../graphify/graphify_integration_test.go | 4 +- packages/go/graphify/graph/graph.go | 4 +- packages/go/schemagen/generator/sql.go | 16 +++ 7 files changed, 201 insertions(+), 8 deletions(-) diff --git a/cmd/api/src/daemons/datapipe/datapipe_integration_test.go b/cmd/api/src/daemons/datapipe/datapipe_integration_test.go index 83de9010eef..68641f87884 100644 --- a/cmd/api/src/daemons/datapipe/datapipe_integration_test.go +++ b/cmd/api/src/daemons/datapipe/datapipe_integration_test.go @@ -90,10 +90,10 @@ func setupIntegrationTestSuite(t *testing.T, fixturesPath string) IntegrationTes err = db.Migrate(ctx) require.NoError(t, err) - err = db.PopulateExtensionData(ctx) + err = graphDB.AssertSchema(ctx, graphschema.DefaultGraphSchema()) require.NoError(t, err) - err = graphDB.AssertSchema(ctx, graphschema.DefaultGraphSchema()) + err = db.PopulateExtensionData(ctx) require.NoError(t, err) ingestSchema, err := upload.LoadIngestSchema() diff --git a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql index 665747958f5..55a4eef09ac 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -15,6 +15,111 @@ -- SPDX-License-Identifier: Apache-2.0 -- Code generated by Cuelang code gen. DO NOT EDIT! -- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ +INSERT INTO kind (name) VALUES + ('Base'), + ('User'), + ('Computer'), + ('Group'), + ('GPO'), + ('OU'), + ('Container'), + ('Domain'), + ('ADLocalGroup'), + ('ADLocalUser'), + ('AIACA'), + ('RootCA'), + ('EnterpriseCA'), + ('NTAuthStore'), + ('CertTemplate'), + ('IssuancePolicy'), + ('Owns'), + ('GenericAll'), + ('GenericWrite'), + ('WriteOwner'), + ('WriteDacl'), + ('MemberOf'), + ('ForceChangePassword'), + ('AllExtendedRights'), + ('AddMember'), + ('HasSession'), + ('Contains'), + ('GPLink'), + ('AllowedToDelegate'), + ('CoerceToTGT'), + ('GetChanges'), + ('GetChangesAll'), + ('GetChangesInFilteredSet'), + ('CrossForestTrust'), + ('SameForestTrust'), + ('SpoofSIDHistory'), + ('AbuseTGTDelegation'), + ('AllowedToAct'), + ('AdminTo'), + ('CanPSRemote'), + ('CanRDP'), + ('ExecuteDCOM'), + ('HasSIDHistory'), + ('AddSelf'), + ('DCSync'), + ('ReadLAPSPassword'), + ('ReadGMSAPassword'), + ('DumpSMSAPassword'), + ('SQLAdmin'), + ('AddAllowedToAct'), + ('WriteSPN'), + ('AddKeyCredentialLink'), + ('LocalToComputer'), + ('MemberOfLocalGroup'), + ('RemoteInteractiveLogonRight'), + ('SyncLAPSPassword'), + ('WriteAccountRestrictions'), + ('WriteGPLink'), + ('RootCAFor'), + ('DCFor'), + ('PublishedTo'), + ('ManageCertificates'), + ('ManageCA'), + ('DelegatedEnrollmentAgent'), + ('Enroll'), + ('HostsCAService'), + ('WritePKIEnrollmentFlag'), + ('WritePKINameFlag'), + ('NTAuthStoreFor'), + ('TrustedForNTAuth'), + ('EnterpriseCAFor'), + ('IssuedSignedBy'), + ('GoldenCert'), + ('EnrollOnBehalfOf'), + ('OIDGroupLink'), + ('ExtendedByPolicy'), + ('ADCSESC1'), + ('ADCSESC3'), + ('ADCSESC4'), + ('ADCSESC6a'), + ('ADCSESC6b'), + ('ADCSESC9a'), + ('ADCSESC9b'), + ('ADCSESC10a'), + ('ADCSESC10b'), + ('ADCSESC13'), + ('SyncedToEntraUser'), + ('CoerceAndRelayNTLMToSMB'), + ('CoerceAndRelayNTLMToADCS'), + ('WriteOwnerLimitedRights'), + ('WriteOwnerRaw'), + ('OwnsLimitedRights'), + ('OwnsRaw'), + ('ClaimSpecialIdentity'), + ('CoerceAndRelayNTLMToLDAP'), + ('CoerceAndRelayNTLMToLDAPS'), + ('ContainsIdentity'), + ('PropagatesACEsTo'), + ('GPOAppliesTo'), + ('CanApplyGPO'), + ('HasTrustKeys'), + ('ProtectAdminGroups') +ON CONFLICT (name) DO NOTHING; + DELETE FROM schema_extensions WHERE name = 'AD'; DO $$ diff --git a/cmd/api/src/database/migration/extensions/az_graph_schema.sql b/cmd/api/src/database/migration/extensions/az_graph_schema.sql index a5854ab3adc..f5654333df5 100644 --- a/cmd/api/src/database/migration/extensions/az_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/az_graph_schema.sql @@ -15,6 +15,78 @@ -- SPDX-License-Identifier: Apache-2.0 -- Code generated by Cuelang code gen. DO NOT EDIT! -- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ +INSERT INTO kind (name) VALUES + ('AZBase'), + ('AZVMScaleSet'), + ('AZApp'), + ('AZRole'), + ('AZDevice'), + ('AZFunctionApp'), + ('AZGroup'), + ('AZKeyVault'), + ('AZManagementGroup'), + ('AZResourceGroup'), + ('AZServicePrincipal'), + ('AZSubscription'), + ('AZTenant'), + ('AZUser'), + ('AZVM'), + ('AZManagedCluster'), + ('AZContainerRegistry'), + ('AZWebApp'), + ('AZLogicApp'), + ('AZAutomationAccount'), + ('AZAvereContributor'), + ('AZContains'), + ('AZContributor'), + ('AZGetCertificates'), + ('AZGetKeys'), + ('AZGetSecrets'), + ('AZHasRole'), + ('AZMemberOf'), + ('AZOwner'), + ('AZRunsAs'), + ('AZVMContributor'), + ('AZAutomationContributor'), + ('AZKeyVaultContributor'), + ('AZVMAdminLogin'), + ('AZAddMembers'), + ('AZAddSecret'), + ('AZExecuteCommand'), + ('AZGlobalAdmin'), + ('AZPrivilegedAuthAdmin'), + ('AZGrant'), + ('AZGrantSelf'), + ('AZPrivilegedRoleAdmin'), + ('AZResetPassword'), + ('AZUserAccessAdministrator'), + ('AZOwns'), + ('AZScopedTo'), + ('AZCloudAppAdmin'), + ('AZAppAdmin'), + ('AZAddOwner'), + ('AZManagedIdentity'), + ('AZMGApplication_ReadWrite_All'), + ('AZMGAppRoleAssignment_ReadWrite_All'), + ('AZMGDirectory_ReadWrite_All'), + ('AZMGGroup_ReadWrite_All'), + ('AZMGGroupMember_ReadWrite_All'), + ('AZMGRoleManagement_ReadWrite_Directory'), + ('AZMGServicePrincipalEndpoint_ReadWrite_All'), + ('AZAKSContributor'), + ('AZNodeResourceGroup'), + ('AZWebsiteContributor'), + ('AZLogicAppContributor'), + ('AZMGAddMember'), + ('AZMGAddOwner'), + ('AZMGAddSecret'), + ('AZMGGrantAppRoles'), + ('AZMGGrantRole'), + ('SyncedToADUser'), + ('AZRoleEligible'), + ('AZRoleApprover') +ON CONFLICT (name) DO NOTHING; + DELETE FROM schema_extensions WHERE name = 'AZ'; DO $$ diff --git a/cmd/api/src/services/entrypoint.go b/cmd/api/src/services/entrypoint.go index 1fa9937ab58..bd03c22db3d 100644 --- a/cmd/api/src/services/entrypoint.go +++ b/cmd/api/src/services/entrypoint.go @@ -80,10 +80,10 @@ func Entrypoint(ctx context.Context, cfg config.Configuration, connections boots if !cfg.DisableMigrations { if err := bootstrap.MigrateDB(ctx, cfg, connections.RDMS, config.NewDefaultAdminConfiguration); err != nil { return nil, fmt.Errorf("rdms migration error: %w", err) - } else if err := bootstrap.PopulateExtensionData(ctx, connections.RDMS); err != nil { - return nil, fmt.Errorf("extensions data population error: %w", err) } else if err := migrations.NewGraphMigrator(connections.Graph).Migrate(ctx); err != nil { return nil, fmt.Errorf("graph migration error: %w", err) + } else if err := bootstrap.PopulateExtensionData(ctx, connections.RDMS); err != nil { + return nil, fmt.Errorf("extensions data population error: %w", err) } } else if err := connections.Graph.SetDefaultGraph(ctx, schema.DefaultGraph()); err != nil { return nil, fmt.Errorf("no default graph found but migrations are disabled per configuration: %w", err) diff --git a/cmd/api/src/services/graphify/graphify_integration_test.go b/cmd/api/src/services/graphify/graphify_integration_test.go index 989155bbeb7..76dc9c9a7cf 100644 --- a/cmd/api/src/services/graphify/graphify_integration_test.go +++ b/cmd/api/src/services/graphify/graphify_integration_test.go @@ -85,10 +85,10 @@ func setupIntegrationTestSuite(t *testing.T, fixturesPath string) IntegrationTes err = db.Migrate(ctx) require.NoError(t, err) - err = db.PopulateExtensionData(ctx) + err = graphDB.AssertSchema(ctx, graphschema.DefaultGraphSchema()) require.NoError(t, err) - err = graphDB.AssertSchema(ctx, graphschema.DefaultGraphSchema()) + err = db.PopulateExtensionData(ctx) require.NoError(t, err) ingestSchema, err := upload.LoadIngestSchema() diff --git a/packages/go/graphify/graph/graph.go b/packages/go/graphify/graph/graph.go index 87437feb911..bd734c9aa2a 100644 --- a/packages/go/graphify/graph/graph.go +++ b/packages/go/graphify/graph/graph.go @@ -175,10 +175,10 @@ func (s *CommunityGraphService) InitializeService(ctx context.Context, connectio if err := s.db.Migrate(ctx); err != nil { return fmt.Errorf("error migrating database: %w", err) - } else if err := s.db.PopulateExtensionData(ctx); err != nil { - return fmt.Errorf("error populating extension data: %w", err) } else if err := migrations.NewGraphMigrator(graphDB).Migrate(ctx); err != nil { return fmt.Errorf("error migrating graph schema: %w", err) + } else if err := s.db.PopulateExtensionData(ctx); err != nil { + return fmt.Errorf("error populating extension data: %w", err) } else if err = graphDB.SetDefaultGraph(ctx, graphschema.DefaultGraph()); err != nil { return fmt.Errorf("error setting default graph: %w", err) } diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index a2a0b882502..234b50f8b74 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -175,6 +175,22 @@ func GenerateExtensionSQL(name string, displayName string, version string, dir s sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/\n", SchemaSourceName)) + sb.WriteString("INSERT INTO kind (name) VALUES\n") + + for _, kind := range nodeKinds { + sb.WriteString(fmt.Sprintf("\t('%s'),\n", kind.GetRepresentation())) + } + + for i, kind := range relationshipKinds { + sb.WriteString(fmt.Sprintf("\t('%s')", kind.GetRepresentation())) + + if i != len(relationshipKinds)-1 { + sb.WriteString(",\n") + } + } + + sb.WriteString("\nON CONFLICT (name) DO NOTHING;\n\n") + sb.WriteString(fmt.Sprintf("DELETE FROM schema_extensions WHERE name = '%s';\n\n", name)) sb.WriteString("DO $$\nDECLARE\n\tnew_extension_id INT;\nBEGIN\n") From 875a397518b717817804dd38a9bda34350a2e9a7 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:40:58 -0500 Subject: [PATCH 13/27] BED-6721: revert code gen --- LICENSE.header | 2 +- cmd/api/src/api/mocks/authenticator.go | 2 +- cmd/api/src/daemons/datapipe/mocks/cleanup.go | 2 +- cmd/api/src/database/mocks/auth.go | 2 +- cmd/api/src/queries/mocks/graph.go | 2 +- cmd/api/src/services/agi/mocks/mock.go | 2 +- cmd/api/src/services/dataquality/mocks/mock.go | 2 +- cmd/api/src/services/graphify/mocks/ingest.go | 2 +- cmd/api/src/services/oidc/mocks/oidc.go | 2 +- cmd/api/src/services/saml/mocks/saml.go | 2 +- cmd/api/src/services/upload/mocks/mock.go | 2 +- cmd/api/src/utils/validation/mocks/validator.go | 2 +- cmd/api/src/vendormocks/dawgs/graph/mock.go | 2 +- cmd/api/src/vendormocks/io/fs/mock.go | 2 +- cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go | 2 +- packages/csharp/graphschema/PropertyNames.cs | 2 +- packages/go/crypto/mocks/digest.go | 2 +- packages/go/graphschema/ad/ad.go | 2 +- packages/go/graphschema/azure/azure.go | 2 +- packages/go/graphschema/common/common.go | 2 +- packages/go/graphschema/graph.go | 2 +- packages/javascript/bh-shared-ui/src/graphSchema.ts | 2 +- .../Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/LICENSE.header b/LICENSE.header index 958be83318a..5d5c596b1c7 100644 --- a/LICENSE.header +++ b/LICENSE.header @@ -1,4 +1,4 @@ -Copyright 2026 Specter Ops, Inc. +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. diff --git a/cmd/api/src/api/mocks/authenticator.go b/cmd/api/src/api/mocks/authenticator.go index 8811703cb3e..32cbd5053e8 100644 --- a/cmd/api/src/api/mocks/authenticator.go +++ b/cmd/api/src/api/mocks/authenticator.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/daemons/datapipe/mocks/cleanup.go b/cmd/api/src/daemons/datapipe/mocks/cleanup.go index d2f7e31808b..ed40825455e 100644 --- a/cmd/api/src/daemons/datapipe/mocks/cleanup.go +++ b/cmd/api/src/daemons/datapipe/mocks/cleanup.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/database/mocks/auth.go b/cmd/api/src/database/mocks/auth.go index d696b4804cc..6934b158d0e 100644 --- a/cmd/api/src/database/mocks/auth.go +++ b/cmd/api/src/database/mocks/auth.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/queries/mocks/graph.go b/cmd/api/src/queries/mocks/graph.go index 85b683c87c1..a536fce424b 100644 --- a/cmd/api/src/queries/mocks/graph.go +++ b/cmd/api/src/queries/mocks/graph.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/agi/mocks/mock.go b/cmd/api/src/services/agi/mocks/mock.go index d8c4c5695b3..d6f0342f8e7 100644 --- a/cmd/api/src/services/agi/mocks/mock.go +++ b/cmd/api/src/services/agi/mocks/mock.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/dataquality/mocks/mock.go b/cmd/api/src/services/dataquality/mocks/mock.go index 0ffa179ce6e..11f0a8a31b5 100644 --- a/cmd/api/src/services/dataquality/mocks/mock.go +++ b/cmd/api/src/services/dataquality/mocks/mock.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/graphify/mocks/ingest.go b/cmd/api/src/services/graphify/mocks/ingest.go index c897f58c6dc..81b68bbc28b 100644 --- a/cmd/api/src/services/graphify/mocks/ingest.go +++ b/cmd/api/src/services/graphify/mocks/ingest.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/oidc/mocks/oidc.go b/cmd/api/src/services/oidc/mocks/oidc.go index 6faa71e5d54..704420ef381 100644 --- a/cmd/api/src/services/oidc/mocks/oidc.go +++ b/cmd/api/src/services/oidc/mocks/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/saml/mocks/saml.go b/cmd/api/src/services/saml/mocks/saml.go index e6029f56541..f71839355b7 100644 --- a/cmd/api/src/services/saml/mocks/saml.go +++ b/cmd/api/src/services/saml/mocks/saml.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/upload/mocks/mock.go b/cmd/api/src/services/upload/mocks/mock.go index a3b90e7fb30..bd1cd940393 100644 --- a/cmd/api/src/services/upload/mocks/mock.go +++ b/cmd/api/src/services/upload/mocks/mock.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/utils/validation/mocks/validator.go b/cmd/api/src/utils/validation/mocks/validator.go index 536c0385a5d..309fe513305 100644 --- a/cmd/api/src/utils/validation/mocks/validator.go +++ b/cmd/api/src/utils/validation/mocks/validator.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/vendormocks/dawgs/graph/mock.go b/cmd/api/src/vendormocks/dawgs/graph/mock.go index 14f78cb94d9..45ea5625414 100644 --- a/cmd/api/src/vendormocks/dawgs/graph/mock.go +++ b/cmd/api/src/vendormocks/dawgs/graph/mock.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/vendormocks/io/fs/mock.go b/cmd/api/src/vendormocks/io/fs/mock.go index f90a77b286f..5c094281bdf 100644 --- a/cmd/api/src/vendormocks/io/fs/mock.go +++ b/cmd/api/src/vendormocks/io/fs/mock.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go b/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go index 1e3a6ab7106..a4adb00b99d 100644 --- a/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go +++ b/cmd/api/src/vendormocks/neo4j/neo4j-go-driver/v5/neo4j/mock.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/csharp/graphschema/PropertyNames.cs b/packages/csharp/graphschema/PropertyNames.cs index 2031b9df97c..fc3d6b08a4f 100644 --- a/packages/csharp/graphschema/PropertyNames.cs +++ b/packages/csharp/graphschema/PropertyNames.cs @@ -1,5 +1,5 @@ /* - Copyright 2026 Specter Ops, Inc. + 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. diff --git a/packages/go/crypto/mocks/digest.go b/packages/go/crypto/mocks/digest.go index ddd118ab958..3c1acaa2cee 100644 --- a/packages/go/crypto/mocks/digest.go +++ b/packages/go/crypto/mocks/digest.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index c9411682e65..c7a589ab217 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/azure/azure.go b/packages/go/graphschema/azure/azure.go index 709eab6828b..3ea14839490 100644 --- a/packages/go/graphschema/azure/azure.go +++ b/packages/go/graphschema/azure/azure.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index 057e552ab76..f18f99a486b 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/graph.go b/packages/go/graphschema/graph.go index acb1385859e..aedef13acd5 100644 --- a/packages/go/graphschema/graph.go +++ b/packages/go/graphschema/graph.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index c92263f53ed..7ede0cf4037 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx index e5d1bb755d0..dab44ff51a6 100644 --- a/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/SelectedDetailsTabs/SelectedDetailsTabs.test.tsx @@ -173,4 +173,4 @@ describe('Selected Details Tabs', () => { expect(objectTab).toBeEnabled(); }); }); -}); +}); \ No newline at end of file From ea47bf0e2d69176f9f3802259cc10c1e3d869cf2 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Tue, 6 Jan 2026 13:00:27 -0500 Subject: [PATCH 14/27] BED-6721: fix license year --- cmd/api/src/database/mocks/db.go | 2 +- cmd/api/src/services/fs/mocks/fs.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index b8f7885ebfb..35af9b7ede7 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. diff --git a/cmd/api/src/services/fs/mocks/fs.go b/cmd/api/src/services/fs/mocks/fs.go index 5ba753d5346..7d05415d188 100644 --- a/cmd/api/src/services/fs/mocks/fs.go +++ b/cmd/api/src/services/fs/mocks/fs.go @@ -1,4 +1,4 @@ -// Copyright 2026 Specter Ops, Inc. +// 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. From 42259861797defc9ff45a090e9c7e65a9630eaab Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:26:56 -0700 Subject: [PATCH 15/27] add postgres functions to insert node and edge kinds --- .../database/migration/migrations/v8.5.0.sql | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index 4705e56462e..2f8ae60a813 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS schema_extensions ( CREATE TABLE IF NOT EXISTS schema_node_kinds ( id SMALLINT PRIMARY KEY REFERENCES kind (id) ON DELETE CASCADE, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this node kind belongs to - name VARCHAR(256) UNIQUE NOT NULL REFERENCES kind (name) ON DELETE CASCADE , -- unique is required by the DAWGS kind table + name VARCHAR(256) UNIQUE NOT NULL, -- unique is required by the DAWGS kind table display_name TEXT NOT NULL, -- can be different from name but usually isn't other than Base/Entity description TEXT NOT NULL, -- human-readable description of the kind is_display_kind BOOL NOT NULL DEFAULT FALSE, @@ -76,7 +76,7 @@ CREATE INDEX IF NOT EXISTS idx_schema_properties_schema_extensions_id on schema_ CREATE TABLE IF NOT EXISTS schema_edge_kinds ( id SMALLINT NOT NULL REFERENCES kind (id) ON DELETE CASCADE, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this edge kind belongs to - name VARCHAR(256) UNIQUE NOT NULL REFERENCES kind (name) ON DELETE CASCADE, -- unique is required by the DAWGS kind table, cypher only allows alphanumeric characters and underscores + name VARCHAR(256) UNIQUE NOT NULL, -- unique is required by the DAWGS kind table, cypher only allows alphanumeric characters and underscores description TEXT NOT NULL, -- human-readable description of the edge-kind is_traversable BOOL NOT NULL DEFAULT FALSE, -- indicates whether the given edge-kind is traversable created_at TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp, @@ -183,3 +183,56 @@ $$ END IF; END $$; + +-- upsert_schema_edge_kind - atomically upserts an edge kind into both the DAWGS kind and schema_edge_kinds tables. +-- This function addresses the edge case where both a schema_edge_kind and schema_node_kind can point to same DAWGS kind. +CREATE OR REPLACE FUNCTION upsert_schema_edge_kind(edge_kind_name TEXT, edge_kind_schema_extension_id INT, edge_kind_description TEXT, edge_kind_is_traversable BOOLEAN) + RETURNS SETOF schema_edge_kinds as $$ +DECLARE + edge_kind_row schema_edge_kinds%ROWTYPE; +BEGIN + IF ( + SELECT EXISTS ( + SELECT 1 + FROM schema_node_kinds + WHERE name = edge_kind_name)) THEN + RAISE EXCEPTION 'duplicate key value violates unique constraint "%", kind already declared in the schema_node_kinds table', edge_kind_name; + END IF; + + WITH dawgs_kinds + AS ( INSERT INTO kind (name) VALUES (edge_kind_name) ON CONFLICT (name) DO UPDATE SET name = edge_kind_name RETURNING id, name) + INSERT + INTO schema_edge_kinds (id, name, schema_extension_id, description, is_traversable) + SELECT id, name, edge_kind_schema_extension_id,edge_kind_description, edge_kind_is_traversable + FROM dawgs_kinds + RETURNING id, schema_extension_id, name, description, is_traversable, created_at, updated_at, deleted_at INTO edge_kind_row; + + RETURN NEXT edge_kind_row; +END $$ LANGUAGE plpgsql; + +-- upsert_schema_node_kind - atomically upserts a node kind into both the DAWGS kind and schema_node_kind tables. +-- This function addresses the edge case where both a schema_edge_kind and schema_node_kind can point to same DAWGS kind. +CREATE OR REPLACE FUNCTION upsert_schema_node_kind(node_kind_name TEXT, node_kind_schema_extension_id INT, node_kind_display_name TEXT, node_kind_description TEXT, node_kind_is_display_kind BOOLEAN, node_kind_icon TEXT, node_kind_icon_color TEXT) + RETURNS SETOF schema_node_kinds as $$ +DECLARE + edge_kind_row schema_node_kinds%ROWTYPE; +BEGIN + IF ( + SELECT EXISTS ( + SELECT 1 + FROM schema_edge_kinds + WHERE name = node_kind_name)) THEN + RAISE EXCEPTION 'duplicate key value violates unique constraint "%", kind already declared in the schema_edge_kinds table', node_kind_name; + END IF; + + WITH dawgs_kinds + AS ( INSERT INTO kind (name) VALUES (node_kind_name) ON CONFLICT (name) DO UPDATE SET name = node_kind_name RETURNING id, name) + INSERT + INTO schema_node_kinds (id, name, schema_extension_id, display_name, description, is_display_kind, icon, + icon_color) + SELECT id, name, node_kind_schema_extension_id, node_kind_display_name, node_kind_description, node_kind_is_display_kind, node_kind_icon, node_kind_icon_color + FROM dawgs_kinds + RETURNING id, schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at INTO edge_kind_row; + + RETURN NEXT edge_kind_row; +END $$ LANGUAGE plpgsql; \ No newline at end of file From 66587107c817a0c2fbb427579c1615d55adb165d Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:29:26 -0700 Subject: [PATCH 16/27] update graph schema functions to use new postgres functions --- cmd/api/src/database/graphschema.go | 30 ++++++----------------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index a724eb05f2b..c80cdfe84bb 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -200,18 +200,9 @@ func (s *BloodhoundDB) DeleteGraphSchemaExtension(ctx context.Context, extension func (s *BloodhoundDB) CreateGraphSchemaNodeKind(ctx context.Context, name string, extensionId int32, displayName string, description string, isDisplayKind bool, icon, iconColor string) (model.GraphSchemaNodeKind, error) { schemaNodeKind := model.GraphSchemaNodeKind{} - // DO UPDATE forces the CTE to return the id and name, DO NOTHING returns an empty id and name, breaking - // the schema table insert - if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` - WITH dawgs_kinds AS ( - INSERT INTO %s (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? - RETURNING id, name - ) - INSERT INTO %s (id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color) - SELECT id, name, ?, ?, ?, ?, ?, ? - FROM dawgs_kinds - RETURNING id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at;`, - kindTable, schemaNodeKind.TableName()), name, name, extensionId, displayName, description, isDisplayKind, icon, iconColor).Scan(&schemaNodeKind); result.Error != nil { + if result := s.db.WithContext(ctx).Raw(` + SELECT id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at + FROM upsert_schema_node_kind(?, ?, ?, ?, ?, ?, ?)`, name, extensionId, displayName, description, isDisplayKind, icon, iconColor).Scan(&schemaNodeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, result.Error) } @@ -408,18 +399,9 @@ func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind - // DO UPDATE forces the CTE to return the id and name, DO NOTHING returns an empty id and name, breaking - // the schema table insert - if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` - WITH dawgs_kinds AS ( - INSERT INTO %s (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? - RETURNING id, name - ) - INSERT INTO %s (id, name, schema_extension_id, description, is_traversable) - SELECT id, name, ?, ?, ? - FROM dawgs_kinds - RETURNING id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at;`, kindTable, - schemaEdgeKind.TableName()), name, name, schemaExtensionId, description, isTraversable).Scan(&schemaEdgeKind); result.Error != nil { + if result := s.db.WithContext(ctx).Raw(` + SELECT id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at + FROM upsert_schema_edge_kind(?, ?, ?, ?);`, 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) } From a76497ff0668aa3caee268157f03b0fc02f6b436 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:30:14 -0700 Subject: [PATCH 17/27] update tests to ensure node and edge kinds cannot share the same DAWGS kind --- .../database/graphschema_integration_test.go | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/cmd/api/src/database/graphschema_integration_test.go b/cmd/api/src/database/graphschema_integration_test.go index d5b090b1321..14ae7b48822 100644 --- a/cmd/api/src/database/graphschema_integration_test.go +++ b/cmd/api/src/database/graphschema_integration_test.go @@ -374,7 +374,7 @@ func TestDatabase_GraphSchemaNodeKind_CRUD(t *testing.T) { }) // Expected fail - return an error if trying to return a node_kind that does not exist t.Run("fail - get a node kind that does not exist", func(t *testing.T) { - _, err = testSuite.BHDatabase.GetGraphSchemaNodeKindById(testSuite.Context, 1112412) + _, err = testSuite.BHDatabase.GetGraphSchemaNodeKindById(testSuite.Context, 112) require.ErrorIs(t, err, database.ErrNotFound) }) @@ -485,7 +485,7 @@ func TestDatabase_GraphSchemaNodeKind_CRUD(t *testing.T) { }) // Expected fail - return an error if trying to update a node_kind that does not exist t.Run("fail - update a node kind that does not exist", func(t *testing.T) { - _, err = testSuite.BHDatabase.UpdateGraphSchemaNodeKind(testSuite.Context, model.GraphSchemaNodeKind{Serial: model.Serial{ID: 123123}, Name: "TEST_KIND_NOT_DUPLICATE", SchemaExtensionId: extension.ID}) + _, err = testSuite.BHDatabase.UpdateGraphSchemaNodeKind(testSuite.Context, model.GraphSchemaNodeKind{Serial: model.Serial{ID: 1223}, Name: "TEST_KIND_NOT_DUPLICATE", SchemaExtensionId: extension.ID}) require.ErrorIs(t, err, database.ErrNotFound) }) @@ -812,7 +812,7 @@ func TestDatabase_GraphSchemaEdgeKind_CRUD(t *testing.T) { }) // Expected fail - return error for if an edge kind that does not exist t.Run("fail - get an edge kind that does not exist", func(t *testing.T) { - _, err = testSuite.BHDatabase.GetGraphSchemaEdgeKindById(testSuite.Context, 23423235) + _, err = testSuite.BHDatabase.GetGraphSchemaEdgeKindById(testSuite.Context, 235) require.ErrorIs(t, err, database.ErrNotFound) }) @@ -920,7 +920,7 @@ func TestDatabase_GraphSchemaEdgeKind_CRUD(t *testing.T) { }) // Expected fail - return an error if trying to update an edge_kind that does not exist t.Run("fail - update an edge kind that does not exist", func(t *testing.T) { - _, err = testSuite.BHDatabase.UpdateGraphSchemaEdgeKind(testSuite.Context, model.GraphSchemaEdgeKind{Serial: model.Serial{ID: 1124123}, Name: edgeKind2.Name, SchemaExtensionId: extension.ID}) + _, err = testSuite.BHDatabase.UpdateGraphSchemaEdgeKind(testSuite.Context, model.GraphSchemaEdgeKind{Serial: model.Serial{ID: 1123}, Name: edgeKind2.Name, SchemaExtensionId: extension.ID}) require.ErrorIs(t, err, database.ErrNotFound) }) @@ -933,11 +933,63 @@ func TestDatabase_GraphSchemaEdgeKind_CRUD(t *testing.T) { }) // Expected fail - return an error if trying to delete an edge_kind that does not exist t.Run("fail - delete an edge kind that does not exist", func(t *testing.T) { - err = testSuite.BHDatabase.DeleteGraphSchemaEdgeKind(testSuite.Context, 1231231) + err = testSuite.BHDatabase.DeleteGraphSchemaEdgeKind(testSuite.Context, 1231) require.ErrorIs(t, err, database.ErrNotFound) }) } +// This test ensures that a schema_node_kind and a schema_edge_kind cannot reference the same DAWGS kind +func TestDatabase_GraphSchemaKind(t *testing.T) { + testSuite := setupIntegrationTestSuite(t) + defer teardownIntegrationTestSuite(t, &testSuite) + extension, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "test_extension_schema_edge_kinds", "test_extension", "1.0.0") + require.NoError(t, err) + + var ( + edgeKind1 = model.GraphSchemaEdgeKind{ + SchemaExtensionId: extension.ID, + Name: "Test_Collision", + } + + nodeKind1 = model.GraphSchemaNodeKind{ + Name: "Test_Collision", + SchemaExtensionId: extension.ID, + } + + edgeKind2 = model.GraphSchemaEdgeKind{ + SchemaExtensionId: extension.ID, + Name: "Test_Collision_2", + } + nodeKind2 = model.GraphSchemaNodeKind{ + Name: "Test_Collision_2", + SchemaExtensionId: extension.ID, + } + ) + + t.Run("fail - unable to create schema_node_kind if kind is already declared as an edge kind", func(t *testing.T) { + _, err = testSuite.BHDatabase.CreateGraphSchemaEdgeKind(testSuite.Context, edgeKind1.Name, edgeKind1.SchemaExtensionId, + edgeKind1.Description, edgeKind1.IsTraversable) + require.NoError(t, err) + + _, err = testSuite.BHDatabase.CreateGraphSchemaNodeKind(testSuite.Context, nodeKind1.Name, nodeKind1.SchemaExtensionId, + nodeKind1.DisplayName, nodeKind1.Description, nodeKind1.IsDisplayKind, nodeKind1.Icon, nodeKind1.IconColor) + require.ErrorContainsf(t, err, database.DuplicateKeyValueErrorString, "expected duplicate key value violates unique constraint") + require.ErrorContainsf(t, err, "kind already declared in the schema_edge_kinds table", "expected duplicate key value violates unique constraint") + + }) + + t.Run("fail - unable to create schema_edge_kind if kind is already declared as a node kind", func(t *testing.T) { + _, err = testSuite.BHDatabase.CreateGraphSchemaNodeKind(testSuite.Context, nodeKind2.Name, nodeKind2.SchemaExtensionId, + nodeKind2.DisplayName, nodeKind2.Description, nodeKind2.IsDisplayKind, nodeKind2.Icon, nodeKind2.IconColor) + require.NoError(t, err) + + _, err = testSuite.BHDatabase.CreateGraphSchemaEdgeKind(testSuite.Context, edgeKind2.Name, edgeKind2.SchemaExtensionId, + edgeKind2.Description, edgeKind2.IsTraversable) + require.ErrorContainsf(t, err, database.DuplicateKeyValueErrorString, "expected duplicate key value violates unique constraint") + require.ErrorContainsf(t, err, "kind already declared in the schema_node_kinds table", "expected duplicate key value violates unique constraint") + }) +} + // compareGraphSchemaNodeKinds - compares the returned list of model.GraphSchemaNodeKinds with the expected results. // Since this is used to compare filtered and paginated results ORDER MATTERS for the expected result. func compareGraphSchemaNodeKinds(t *testing.T, got, want model.GraphSchemaNodeKinds) { From 31daabff4121a202c411ac9d8ac7f6f21973b9b9 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:00:54 -0700 Subject: [PATCH 18/27] wip --- cmd/api/src/database/migration/migrations/v8.5.0.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index 2f8ae60a813..994a0e45d23 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -215,7 +215,7 @@ END $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION upsert_schema_node_kind(node_kind_name TEXT, node_kind_schema_extension_id INT, node_kind_display_name TEXT, node_kind_description TEXT, node_kind_is_display_kind BOOLEAN, node_kind_icon TEXT, node_kind_icon_color TEXT) RETURNS SETOF schema_node_kinds as $$ DECLARE - edge_kind_row schema_node_kinds%ROWTYPE; + node_kind_row schema_node_kinds%ROWTYPE; BEGIN IF ( SELECT EXISTS ( @@ -232,7 +232,7 @@ BEGIN icon_color) SELECT id, name, node_kind_schema_extension_id, node_kind_display_name, node_kind_description, node_kind_is_display_kind, node_kind_icon, node_kind_icon_color FROM dawgs_kinds - RETURNING id, schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at INTO edge_kind_row; + RETURNING id, schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at INTO node_kind_row; - RETURN NEXT edge_kind_row; + RETURN NEXT node_kind_row; END $$ LANGUAGE plpgsql; \ No newline at end of file From fd3b84e3a99ee5c13e673308f81f16897691fb0d Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:05:05 -0700 Subject: [PATCH 19/27] add table lock to insert node and edge kinds --- cmd/api/src/database/migration/migrations/v8.5.0.sql | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index 994a0e45d23..ceb0d236f81 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -191,6 +191,9 @@ CREATE OR REPLACE FUNCTION upsert_schema_edge_kind(edge_kind_name TEXT, edge_kin DECLARE edge_kind_row schema_edge_kinds%ROWTYPE; BEGIN + + LOCK TABLE schema_node_kinds, schema_edge_kinds IN EXCLUSIVE MODE; + IF ( SELECT EXISTS ( SELECT 1 @@ -217,6 +220,9 @@ CREATE OR REPLACE FUNCTION upsert_schema_node_kind(node_kind_name TEXT, node_kin DECLARE node_kind_row schema_node_kinds%ROWTYPE; BEGIN + + LOCK TABLE schema_node_kinds, schema_edge_kinds IN EXCLUSIVE MODE; + IF ( SELECT EXISTS ( SELECT 1 From 81fe5b8e1d2b76d9c03147a519de42590b75e317 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:54:55 -0700 Subject: [PATCH 20/27] Update graph schema node and edge functions and tables to use a normalized kind_id column as FK to DAWGS --- cmd/api/src/database/graphschema.go | 157 +++++++++++++----- .../database/migration/migrations/v8.5.0.sql | 105 ++++++++---- 2 files changed, 180 insertions(+), 82 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index fdc92d42eeb..edbc0c81c2d 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -204,17 +204,25 @@ func (s *BloodhoundDB) DeleteGraphSchemaExtension(ctx context.Context, extension // Since this inserts directly into the kinds table, the business logic calling this func // must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaNodeKind(ctx context.Context, name string, extensionId int32, displayName string, description string, isDisplayKind bool, icon, iconColor string) (model.GraphSchemaNodeKind, error) { - schemaNodeKind := model.GraphSchemaNodeKind{} - - if result := s.db.WithContext(ctx).Raw(` - SELECT id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at - FROM upsert_schema_node_kind(?, ?, ?, ?, ?, ?, ?)`, name, extensionId, displayName, description, isDisplayKind, icon, iconColor).Scan(&schemaNodeKind); result.Error != nil { - if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { - return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, result.Error) + var schemaNodeKind = model.GraphSchemaNodeKind{} + if db, err := s.db.DB(); err != nil { + return schemaNodeKind, err + } else if row := db.QueryRowContext(ctx, ` + SELECT schema_node_kind_id, return_schema_extension_id, return_name, return_display_name, return_description, + return_is_display_kind, return_icon, return_icon_color, return_created_at, return_updated_at, return_deleted_at + FROM upsert_schema_node_kind($1, $2, $3, $4, $5, $6, $7)`, name, extensionId, displayName, description, isDisplayKind, icon, iconColor); row.Err() != nil { + if strings.Contains(row.Err().Error(), DuplicateKeyValueErrorString) { + return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, row.Err().Error()) } - return model.GraphSchemaNodeKind{}, CheckError(result) + return model.GraphSchemaNodeKind{}, row.Err() + } else { + if err := row.Scan(&schemaNodeKind.ID, &schemaNodeKind.SchemaExtensionId, &schemaNodeKind.Name, &schemaNodeKind.DisplayName, + &schemaNodeKind.Description, &schemaNodeKind.IsDisplayKind, &schemaNodeKind.Icon, &schemaNodeKind.IconColor, + &schemaNodeKind.CreatedAt, &schemaNodeKind.UpdatedAt, &schemaNodeKind.DeletedAt); err != nil { + return model.GraphSchemaNodeKind{}, err + } + return schemaNodeKind, nil } - return schemaNodeKind, nil } // GetGraphSchemaNodeKinds - returns all rows from the schema_node_kinds table that matches the given model.Filters. It returns a slice of model.GraphSchemaNodeKinds structs @@ -225,16 +233,32 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters mode totalRowCount int ) + for column := range filters { + if column == "name" { + column = fmt.Sprintf("%s.%s", kindTable, column) + } else { + column = fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), column) + + } + } + + for _, sortItem := range sort { + if sortItem.Column == "name" { + sortItem.Column = fmt.Sprintf("%s.%s", kindTable, sortItem.Column) + } else { + sortItem.Column = fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), sortItem.Column) + } + } + if filterAndPagination, err := parseFiltersAndPagination(filters, sort, skip, limit); err != nil { return schemaNodeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at - FROM %s %s %s %s`, - model.GraphSchemaNodeKind{}.TableName(), - filterAndPagination.WhereClause, - filterAndPagination.OrderSql, - filterAndPagination.SkipLimit) - + sqlStr := fmt.Sprintf(`SELECT %s.id, %s.name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at + FROM %s + JOIN %s ON %s.kind_id = %s.id + %s %s %s`, + model.GraphSchemaNodeKind{}.TableName(), kindTable, model.GraphSchemaNodeKind{}.TableName(), kindTable, + 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 { return nil, 0, CheckError(result) } else { @@ -256,8 +280,9 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters mode func (s *BloodhoundDB) GetGraphSchemaNodeKindById(ctx context.Context, schemaNodeKindId int32) (model.GraphSchemaNodeKind, error) { var schemaNodeKind model.GraphSchemaNodeKind return schemaNodeKind, CheckError(s.db.WithContext(ctx).Raw(fmt.Sprintf(` - SELECT id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at - FROM %s WHERE id = ?`, schemaNodeKind.TableName()), schemaNodeKindId).First(&schemaNodeKind)) + SELECT %s.id, name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at + FROM %s JOIN %s ON %s.kind_id = %s.id WHERE %s.id = ?`, schemaNodeKind.TableName(), schemaNodeKind.TableName(), kindTable, + schemaNodeKind.TableName(), kindTable, schemaNodeKind.TableName()), schemaNodeKindId).First(&schemaNodeKind)) } // UpdateGraphSchemaNodeKind - updates a row in the schema_node_kinds table based on the provided id. It will return an @@ -267,11 +292,18 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKindById(ctx context.Context, schemaNod // table is append only. A new node kind should be created instead. func (s *BloodhoundDB) UpdateGraphSchemaNodeKind(ctx context.Context, schemaNodeKind model.GraphSchemaNodeKind) (model.GraphSchemaNodeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` - UPDATE %s - SET 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`, - schemaNodeKind.TableName()), schemaNodeKind.SchemaExtensionId, schemaNodeKind.DisplayName, schemaNodeKind.Description, schemaNodeKind.IsDisplayKind, schemaNodeKind.Icon, schemaNodeKind.IconColor, schemaNodeKind.ID).Scan(&schemaNodeKind); result.Error != nil { + WITH updated_row AS ( + UPDATE %s + SET schema_extension_id = ?, display_name = ?, description = ?, is_display_kind = ?, icon = ?, icon_color = ?, updated_at = NOW() + WHERE id = ? + 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 + JOIN %s ON %s.id = updated_row.kind_id`, + schemaNodeKind.TableName(), kindTable, kindTable, kindTable), schemaNodeKind.SchemaExtensionId, + schemaNodeKind.DisplayName, schemaNodeKind.Description, schemaNodeKind.IsDisplayKind, schemaNodeKind.Icon, + schemaNodeKind.IconColor, schemaNodeKind.ID).Scan(&schemaNodeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, result.Error) } @@ -363,7 +395,8 @@ func (s *BloodhoundDB) GetGraphSchemaPropertyById(ctx context.Context, extension return extensionProperty, nil } -// UpdateGraphSchemaProperty - updates a row in the schema_properties table based on the provided id. It will return an error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. +// UpdateGraphSchemaProperty - updates a row in the schema_properties table based on the provided id. It will return an +// error if the target property does not exist or if any of the updates violate the schema constraints. 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 = ?, schema_extension_id = ?, display_name = ?, data_type = ?, description = ?, updated_at = NOW() WHERE id = ? @@ -405,15 +438,23 @@ func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind - if result := s.db.WithContext(ctx).Raw(` - SELECT id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at - FROM upsert_schema_edge_kind(?, ?, ?, ?);`, 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) + if db, err := s.db.DB(); err != nil { + return model.GraphSchemaEdgeKind{}, err + } else if row := db.QueryRowContext(ctx, ` + SELECT schema_edge_kind_id, return_schema_extension_id, return_name, return_description, return_is_traversable, + return_created_at, return_updated_at, return_deleted_at + FROM upsert_schema_edge_kind($1, $2, $3, $4);`, name, schemaExtensionId, description, isTraversable); row.Err() != nil { + if strings.Contains(row.Err().Error(), DuplicateKeyValueErrorString) { + return schemaEdgeKind, fmt.Errorf("%w: %v", ErrDuplicateSchemaEdgeKindName, row.Err().Error()) } - return schemaEdgeKind, CheckError(result) + return schemaEdgeKind, row.Err() + } else { + if err = row.Scan(&schemaEdgeKind.ID, &schemaEdgeKind.SchemaExtensionId, &schemaEdgeKind.Name, &schemaEdgeKind.Description, + &schemaEdgeKind.IsTraversable, &schemaEdgeKind.CreatedAt, &schemaEdgeKind.UpdatedAt, &schemaEdgeKind.DeletedAt); err != nil { + return model.GraphSchemaEdgeKind{}, err + } + return schemaEdgeKind, nil } - return schemaEdgeKind, nil } // GetGraphSchemaEdgeKinds - returns all rows from the schema_edge_kinds table that matches the given model.Filters. It returns a slice of model.GraphSchemaEdgeKinds @@ -424,16 +465,32 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilt totalRowCount int ) + for column := range edgeKindFilters { + if column == "name" { + column = fmt.Sprintf("%s.%s", kindTable, column) + } else { + column = fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), column) + + } + } + + for _, sortItem := range sort { + if sortItem.Column == "name" { + sortItem.Column = fmt.Sprintf("%s.%s", kindTable, sortItem.Column) + } else { + sortItem.Column = fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), sortItem.Column) + } + } + if filterAndPagination, err := parseFiltersAndPagination(edgeKindFilters, sort, skip, limit); err != nil { return schemaEdgeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at - FROM %s %s %s %s`, - model.GraphSchemaEdgeKind{}.TableName(), - filterAndPagination.WhereClause, - filterAndPagination.OrderSql, - filterAndPagination.SkipLimit) - + sqlStr := fmt.Sprintf(`SELECT %s.id, %s.name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at + FROM %s + JOIN %s ON %s.kind_id = %s.id + %s %s %s`, + model.GraphSchemaEdgeKind{}.TableName(), kindTable, model.GraphSchemaEdgeKind{}.TableName(), kindTable, + model.GraphSchemaEdgeKind{}.TableName(), kindTable, filterAndPagination.WhereClause, filterAndPagination.OrderSql, filterAndPagination.SkipLimit) if result := s.db.WithContext(ctx).Raw(sqlStr, filterAndPagination.Filter.params...).Scan(&schemaEdgeKinds); result.Error != nil { return nil, 0, CheckError(result) } else { @@ -460,10 +517,11 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKindsWithSchemaName(ctx context.Context if filterAndPagination, err := parseFiltersAndPagination(edgeKindFilters, sort, skip, limit); err != nil { return schemaEdgeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT edge.id, edge.name, edge.description, edge.is_traversable, schema.name as schema_name - FROM %s edge JOIN %s schema ON edge.schema_extension_id = schema.id %s %s %s`, + sqlStr := fmt.Sprintf(`SELECT edge.id, k.name, edge.description, edge.is_traversable, schema.name as schema_name + FROM %s edge JOIN %s schema ON edge.schema_extension_id = schema.id JOIN %s k ON edge.kind_id = k.id %s %s %s`, model.GraphSchemaEdgeKind{}.TableName(), model.GraphSchemaExtension{}.TableName(), + kindTable, filterAndPagination.WhereClause, filterAndPagination.OrderSql, filterAndPagination.SkipLimit) @@ -492,8 +550,9 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKindsWithSchemaName(ctx context.Context func (s *BloodhoundDB) GetGraphSchemaEdgeKindById(ctx context.Context, schemaEdgeKindId int32) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind return schemaEdgeKind, CheckError(s.db.WithContext(ctx).Raw(fmt.Sprintf(` - SELECT id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at - FROM %s WHERE id = ?`, schemaEdgeKind.TableName()), schemaEdgeKindId).First(&schemaEdgeKind)) + SELECT %s.id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at + FROM %s JOIN %s ON %s.kind_id = %s.id WHERE %s.id = ?`, schemaEdgeKind.TableName(), schemaEdgeKind.TableName(), kindTable, + schemaEdgeKind.TableName(), kindTable, schemaEdgeKind.TableName()), schemaEdgeKindId).First(&schemaEdgeKind)) } // UpdateGraphSchemaEdgeKind - updates a row in the schema_edge_kinds table based on the provided id. It will return an @@ -503,10 +562,16 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKindById(ctx context.Context, schemaEdg // table is append only. A new edge kind should be created instead. func (s *BloodhoundDB) UpdateGraphSchemaEdgeKind(ctx context.Context, schemaEdgeKind model.GraphSchemaEdgeKind) (model.GraphSchemaEdgeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` - UPDATE %s - SET schema_extension_id = ?, description = ?, is_traversable = ?, updated_at = NOW() - WHERE id = ? - RETURNING id, name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at`, schemaEdgeKind.TableName()), + WITH updated_row as ( + UPDATE %s + 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`, + schemaEdgeKind.TableName(), kindTable, kindTable, kindTable), schemaEdgeKind.SchemaExtensionId, schemaEdgeKind.Description, schemaEdgeKind.IsTraversable, schemaEdgeKind.ID).Scan(&schemaEdgeKind); result.Error != nil { if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index ceb0d236f81..5ba4448ab36 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -40,9 +40,9 @@ CREATE TABLE IF NOT EXISTS schema_extensions ( -- OpenGraph schema_node_kinds - stores node kinds for open graph extensions. This FK's to the DAWGS kind table directly. CREATE TABLE IF NOT EXISTS schema_node_kinds ( - id SMALLINT PRIMARY KEY REFERENCES kind (id) ON DELETE CASCADE, + id SERIAL PRIMARY KEY, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this node kind belongs to - name VARCHAR(256) UNIQUE NOT NULL, -- unique is required by the DAWGS kind table + kind_id SMALLINT NOT NULL UNIQUE REFERENCES kind (id) ON DELETE CASCADE, display_name TEXT NOT NULL, -- can be different from name but usually isn't other than Base/Entity description TEXT NOT NULL, -- human-readable description of the kind is_display_kind BOOL NOT NULL DEFAULT FALSE, @@ -74,15 +74,14 @@ CREATE INDEX IF NOT EXISTS idx_schema_properties_schema_extensions_id on schema_ -- OpenGraph schema_edge_kinds - store edge kinds for open graph extensions. This FK's to the DAWGS kind table directly. CREATE TABLE IF NOT EXISTS schema_edge_kinds ( - id SMALLINT NOT NULL REFERENCES kind (id) ON DELETE CASCADE, + id SERIAL PRIMARY KEY, schema_extension_id INT NOT NULL REFERENCES schema_extensions (id) ON DELETE CASCADE, -- indicates which extension this edge kind belongs to - name VARCHAR(256) UNIQUE NOT NULL, -- unique is required by the DAWGS kind table, cypher only allows alphanumeric characters and underscores + kind_id SMALLINT NOT NULL UNIQUE REFERENCES kind (id) ON DELETE CASCADE, description TEXT NOT NULL, -- human-readable description of the edge-kind is_traversable BOOL NOT NULL DEFAULT FALSE, -- indicates whether the given edge-kind is traversable 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) + deleted_at TIMESTAMP WITH TIME ZONE DEFAULT NULL ); CREATE INDEX IF NOT EXISTS idx_schema_edge_kinds_extensions_id ON schema_edge_kinds (schema_extension_id); @@ -186,59 +185,93 @@ $$; -- upsert_schema_edge_kind - atomically upserts an edge kind into both the DAWGS kind and schema_edge_kinds tables. -- This function addresses the edge case where both a schema_edge_kind and schema_node_kind can point to same DAWGS kind. -CREATE OR REPLACE FUNCTION upsert_schema_edge_kind(edge_kind_name TEXT, edge_kind_schema_extension_id INT, edge_kind_description TEXT, edge_kind_is_traversable BOOLEAN) - RETURNS SETOF schema_edge_kinds as $$ -DECLARE - edge_kind_row schema_edge_kinds%ROWTYPE; +CREATE OR REPLACE FUNCTION upsert_schema_edge_kind(edge_kind_name TEXT, edge_kind_schema_extension_id INT, + edge_kind_description TEXT, edge_kind_is_traversable BOOLEAN) + RETURNS TABLE ( + schema_edge_kind_id INT, + return_schema_extension_id INT, + return_name TEXT, + return_description TEXT, + return_is_traversable BOOL, + return_created_at TIMESTAMP WITH TIME ZONE, + return_updated_at TIMESTAMP WITH TIME ZONE, + return_deleted_at TIMESTAMP WITH TIME ZONE + ) +as $$ BEGIN - LOCK TABLE schema_node_kinds, schema_edge_kinds IN EXCLUSIVE MODE; + LOCK TABLE schema_node_kinds, schema_edge_kinds IN EXCLUSIVE MODE; -- DAWGS Kind table is append only so no need to lock IF ( SELECT EXISTS ( SELECT 1 - FROM schema_node_kinds - WHERE name = edge_kind_name)) THEN + FROM schema_node_kinds snk + JOIN kind k ON snk.kind_id = k.id + WHERE k.name = edge_kind_name)) THEN RAISE EXCEPTION 'duplicate key value violates unique constraint "%", kind already declared in the schema_node_kinds table', edge_kind_name; END IF; - WITH dawgs_kinds - AS ( INSERT INTO kind (name) VALUES (edge_kind_name) ON CONFLICT (name) DO UPDATE SET name = edge_kind_name RETURNING id, name) - INSERT - INTO schema_edge_kinds (id, name, schema_extension_id, description, is_traversable) - SELECT id, name, edge_kind_schema_extension_id,edge_kind_description, edge_kind_is_traversable + RETURN QUERY + WITH dawgs_kinds AS + ( INSERT INTO kind (name) VALUES (edge_kind_name) ON CONFLICT (name) DO UPDATE SET name = edge_kind_name RETURNING id, name) + INSERT INTO schema_edge_kinds (kind_id, schema_extension_id, description, is_traversable) + SELECT id, + edge_kind_schema_extension_id, + edge_kind_description, + edge_kind_is_traversable FROM dawgs_kinds - RETURNING id, schema_extension_id, name, description, is_traversable, created_at, updated_at, deleted_at INTO edge_kind_row; - - RETURN NEXT edge_kind_row; -END $$ LANGUAGE plpgsql; + RETURNING id, schema_extension_id, edge_kind_name, description, is_traversable, created_at, updated_at, deleted_at; +END +$$ LANGUAGE plpgsql; -- upsert_schema_node_kind - atomically upserts a node kind into both the DAWGS kind and schema_node_kind tables. -- This function addresses the edge case where both a schema_edge_kind and schema_node_kind can point to same DAWGS kind. -CREATE OR REPLACE FUNCTION upsert_schema_node_kind(node_kind_name TEXT, node_kind_schema_extension_id INT, node_kind_display_name TEXT, node_kind_description TEXT, node_kind_is_display_kind BOOLEAN, node_kind_icon TEXT, node_kind_icon_color TEXT) - RETURNS SETOF schema_node_kinds as $$ -DECLARE - node_kind_row schema_node_kinds%ROWTYPE; +CREATE OR REPLACE FUNCTION upsert_schema_node_kind(node_kind_name TEXT, node_kind_schema_extension_id INT, + node_kind_display_name TEXT, node_kind_description TEXT, + node_kind_is_display_kind BOOLEAN, node_kind_icon TEXT, + node_kind_icon_color TEXT) + RETURNS TABLE ( + schema_node_kind_id INT, + return_schema_extension_id INT, + return_name TEXT, + return_display_name TEXT, + return_description TEXT, + return_is_display_kind bool, + return_icon TEXT, + return_icon_color TEXT, + return_created_at TIMESTAMP WITH TIME ZONE, + return_updated_at TIMESTAMP WITH TIME ZONE, + return_deleted_at TIMESTAMP WITH TIME ZONE + ) +as $$ BEGIN - LOCK TABLE schema_node_kinds, schema_edge_kinds IN EXCLUSIVE MODE; + LOCK TABLE schema_node_kinds, schema_edge_kinds IN EXCLUSIVE MODE; -- DAWGS Kind table is append only so no need to lock IF ( SELECT EXISTS ( SELECT 1 - FROM schema_edge_kinds - WHERE name = node_kind_name)) THEN + FROM schema_edge_kinds sek + JOIN kind k ON sek.kind_id = k.id + WHERE k.name = node_kind_name)) THEN RAISE EXCEPTION 'duplicate key value violates unique constraint "%", kind already declared in the schema_edge_kinds table', node_kind_name; END IF; + RETURN QUERY WITH dawgs_kinds AS ( INSERT INTO kind (name) VALUES (node_kind_name) ON CONFLICT (name) DO UPDATE SET name = node_kind_name RETURNING id, name) INSERT - INTO schema_node_kinds (id, name, schema_extension_id, display_name, description, is_display_kind, icon, + INTO schema_node_kinds (kind_id, schema_extension_id, display_name, description, is_display_kind, icon, icon_color) - SELECT id, name, node_kind_schema_extension_id, node_kind_display_name, node_kind_description, node_kind_is_display_kind, node_kind_icon, node_kind_icon_color - FROM dawgs_kinds - RETURNING id, schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at INTO node_kind_row; - - RETURN NEXT node_kind_row; -END $$ LANGUAGE plpgsql; \ No newline at end of file + SELECT dk.id, + node_kind_schema_extension_id, + node_kind_display_name, + node_kind_description, + node_kind_is_display_kind, + node_kind_icon, + node_kind_icon_color + FROM dawgs_kinds dk + RETURNING id, schema_extension_id, node_kind_name, display_name, description, is_display_kind, + icon, icon_color, created_at, updated_at, deleted_at; +END +$$ LANGUAGE plpgsql; \ No newline at end of file From babaf4ea4c348350a7ae64ea2145e42511396752 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:35:11 -0700 Subject: [PATCH 21/27] ensure map assignments work --- cmd/api/src/database/graphschema.go | 34 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index edbc0c81c2d..648802a6002 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -233,20 +233,21 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters mode totalRowCount int ) - for column := range filters { + for column, filter := range filters { if column == "name" { - column = fmt.Sprintf("%s.%s", kindTable, column) + filters[fmt.Sprintf("%s.%s", kindTable, column)] = filter + delete(filters, column) } else { - column = fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), column) - + filters[fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), column)] = filter + delete(filters, column) } } - for _, sortItem := range sort { - if sortItem.Column == "name" { - sortItem.Column = fmt.Sprintf("%s.%s", kindTable, sortItem.Column) + for idx, _ := range sort { + if sort[idx].Column == "name" { + sort[idx].Column = fmt.Sprintf("%s.%s", kindTable, sort[idx].Column) } else { - sortItem.Column = fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), sortItem.Column) + sort[idx].Column = fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), sort[idx].Column) } } @@ -465,20 +466,21 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilt totalRowCount int ) - for column := range edgeKindFilters { + for column, filters := range edgeKindFilters { if column == "name" { - column = fmt.Sprintf("%s.%s", kindTable, column) + edgeKindFilters[fmt.Sprintf("%s.%s", kindTable, column)] = filters + delete(edgeKindFilters, column) } else { - column = fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), column) - + edgeKindFilters[fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), column)] = filters + delete(edgeKindFilters, column) } } - for _, sortItem := range sort { - if sortItem.Column == "name" { - sortItem.Column = fmt.Sprintf("%s.%s", kindTable, sortItem.Column) + for idx, _ := range sort { + if sort[idx].Column == "name" { + sort[idx].Column = fmt.Sprintf("%s.%s", kindTable, sort[idx].Column) } else { - sortItem.Column = fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), sortItem.Column) + sort[idx].Column = fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), sort[idx].Column) } } From 303e1c22105d7d5ba33afedefb0e76f06e9d5570 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:24:46 -0700 Subject: [PATCH 22/27] add more documentation --- cmd/api/src/database/graphschema.go | 6 ++++-- .../src/database/migration/migrations/v8.5.0.sql | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 648802a6002..7918503e785 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -233,6 +233,7 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters mode totalRowCount int ) + // add table identifiers to filtering and sorting columns ensuring we don't return an ambiguous column error for column, filter := range filters { if column == "name" { filters[fmt.Sprintf("%s.%s", kindTable, column)] = filter @@ -289,7 +290,7 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKindById(ctx context.Context, schemaNod // UpdateGraphSchemaNodeKind - 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. // -// This function does NOT update the name column since the schema_node_kinds table FKs to the DAWGS kind table, and that +// This function does NOT update the DAWGS name column since the schema_node_kinds table FKs to the DAWGS kind table, and that // table is append only. A new node kind should be created instead. func (s *BloodhoundDB) UpdateGraphSchemaNodeKind(ctx context.Context, schemaNodeKind model.GraphSchemaNodeKind) (model.GraphSchemaNodeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` @@ -466,6 +467,7 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilt totalRowCount int ) + // add table identifiers to filtering and sorting columns ensuring we don't return an ambiguous column error for column, filters := range edgeKindFilters { if column == "name" { edgeKindFilters[fmt.Sprintf("%s.%s", kindTable, column)] = filters @@ -560,7 +562,7 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKindById(ctx context.Context, schemaEdg // UpdateGraphSchemaEdgeKind - updates a row in the schema_edge_kinds table based on the provided id. It will return an // error if the target schema edge kind does not exist or if any of the updates violate the schema constraints. // -// This function does NOT update the name column since the schema_edge_kinds table FKs to the DAWGS kind table, and that +// This function does NOT update the DAWGS name column since the schema_edge_kinds table FKs to the DAWGS kind table, and that // table is append only. A new edge kind should be created instead. func (s *BloodhoundDB) UpdateGraphSchemaEdgeKind(ctx context.Context, schemaEdgeKind model.GraphSchemaEdgeKind) (model.GraphSchemaEdgeKind, error) { if result := s.db.WithContext(ctx).Raw(fmt.Sprintf(` diff --git a/cmd/api/src/database/migration/migrations/v8.5.0.sql b/cmd/api/src/database/migration/migrations/v8.5.0.sql index 5ba4448ab36..6ed00977b18 100644 --- a/cmd/api/src/database/migration/migrations/v8.5.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.5.0.sql @@ -185,6 +185,12 @@ $$; -- upsert_schema_edge_kind - atomically upserts an edge kind into both the DAWGS kind and schema_edge_kinds tables. -- This function addresses the edge case where both a schema_edge_kind and schema_node_kind can point to same DAWGS kind. +-- First, the function locks the schema_node_kinds and schema_edge_kinds tables so that another insert won't add a kind during this function. +-- Second, the function checks the schema_node_kinds table to ensure the kind isnt already defined there. +-- Assuming the kind is unique, it will then insert the kind into the DAWGS table. Since the DAWGS table is append-only +-- the ON CONFLICT DO UPDATE is used to ensure we get the id and name back even if it already exists (without an error). +-- Following this, we insert into the schema_edge_kinds table and return the values needed to fill out a model.GraphSchemaEdgeKind. +-- The return table columns must be unique otherwise Postgres will return an error for ambiguous column names. CREATE OR REPLACE FUNCTION upsert_schema_edge_kind(edge_kind_name TEXT, edge_kind_schema_extension_id INT, edge_kind_description TEXT, edge_kind_is_traversable BOOLEAN) RETURNS TABLE ( @@ -226,6 +232,12 @@ $$ LANGUAGE plpgsql; -- upsert_schema_node_kind - atomically upserts a node kind into both the DAWGS kind and schema_node_kind tables. -- This function addresses the edge case where both a schema_edge_kind and schema_node_kind can point to same DAWGS kind. +-- First, the function locks the schema_node_kinds and schema_edge_kinds tables so that another insert won't add a kind during this function. +-- Second, the function checks the schema_edge_kinds table to ensure the kind isnt already defined there. +-- Assuming the kind is unique, it will then insert the kind into the DAWGS table. Since the DAWGS table is append-only +-- the ON CONFLICT DO UPDATE is used to ensure we get the id and name back even if it already exists (without erroring). +-- Following this, we insert into the schema_node_kinds table and return the values needed to fill out a model.GraphSchemaNodeKind +-- The return table columns must be unique otherwise Postgres will return an error for ambiguous column names. CREATE OR REPLACE FUNCTION upsert_schema_node_kind(node_kind_name TEXT, node_kind_schema_extension_id INT, node_kind_display_name TEXT, node_kind_description TEXT, node_kind_is_display_kind BOOLEAN, node_kind_icon TEXT, @@ -261,8 +273,7 @@ BEGIN WITH dawgs_kinds AS ( INSERT INTO kind (name) VALUES (node_kind_name) ON CONFLICT (name) DO UPDATE SET name = node_kind_name RETURNING id, name) INSERT - INTO schema_node_kinds (kind_id, schema_extension_id, display_name, description, is_display_kind, icon, - icon_color) + INTO schema_node_kinds (kind_id, schema_extension_id, display_name, description, is_display_kind, icon, icon_color) SELECT dk.id, node_kind_schema_extension_id, node_kind_display_name, From 6488e3c7b1a2da716ef490718e297674a5c2f6e9 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:24:54 -0700 Subject: [PATCH 23/27] update filtering and sorting to include table identifiers --- cmd/api/src/database/graphschema.go | 75 ++++++++++++------- .../database/graphschema_integration_test.go | 4 +- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 7918503e785..2f6291b636b 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -229,43 +229,52 @@ func (s *BloodhoundDB) CreateGraphSchemaNodeKind(ctx context.Context, name strin // populated with data, as well as an integer indicating the total number of rows returned by the query (excluding any given pagination). func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaNodeKinds, int, error) { var ( - schemaNodeKinds = model.GraphSchemaNodeKinds{} - totalRowCount int + schemaNodeKinds = model.GraphSchemaNodeKinds{} + totalRowCount int + tableIdentifiedFilters = make(model.Filters, len(filters)) + tableIdentifiedSort = make(model.Sort, len(sort)) ) // add table identifiers to filtering and sorting columns ensuring we don't return an ambiguous column error for column, filter := range filters { if column == "name" { - filters[fmt.Sprintf("%s.%s", kindTable, column)] = filter + tableIdentifiedFilters[fmt.Sprintf("%s.%s", "k", column)] = filter delete(filters, column) } else { - filters[fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), column)] = filter + tableIdentifiedFilters[fmt.Sprintf("%s.%s", "nk", column)] = filter delete(filters, column) } } - for idx, _ := range sort { + for idx, sortItem := range sort { if sort[idx].Column == "name" { - sort[idx].Column = fmt.Sprintf("%s.%s", kindTable, sort[idx].Column) + tableIdentifiedSort[idx] = model.SortItem{ + Direction: sortItem.Direction, + Column: fmt.Sprintf("%s.%s", "k", sort[idx].Column), + } } else { - sort[idx].Column = fmt.Sprintf("%s.%s", model.GraphSchemaNodeKind{}.TableName(), sort[idx].Column) + tableIdentifiedSort[idx] = model.SortItem{ + Direction: sortItem.Direction, + Column: fmt.Sprintf("%s.%s", "nk", sort[idx].Column), + } } } - if filterAndPagination, err := parseFiltersAndPagination(filters, sort, skip, limit); err != nil { + if filterAndPagination, err := parseFiltersAndPagination(tableIdentifiedFilters, tableIdentifiedSort, skip, limit); err != nil { return schemaNodeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT %s.id, %s.name, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at - FROM %s - JOIN %s ON %s.kind_id = %s.id + 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 %s %s %s`, - model.GraphSchemaNodeKind{}.TableName(), kindTable, model.GraphSchemaNodeKind{}.TableName(), kindTable, 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 { return nil, 0, CheckError(result) } else { if limit > 0 || skip > 0 { - countSqlStr := fmt.Sprintf(`SELECT COUNT(*) FROM %s %s`, model.GraphSchemaNodeKind{}.TableName(), filterAndPagination.WhereClause) + countSqlStr := fmt.Sprintf(`SELECT COUNT(*) FROM %s nk JOIN %s k ON nk.kind_id = k.id %s`, + model.GraphSchemaNodeKind{}.TableName(), kindTable, filterAndPagination.WhereClause) if countResult := s.db.WithContext(ctx).Raw(countSqlStr, filterAndPagination.Filter.params...).Scan(&totalRowCount); countResult.Error != nil { return model.GraphSchemaNodeKinds{}, 0, CheckError(countResult) } @@ -275,7 +284,6 @@ func (s *BloodhoundDB) GetGraphSchemaNodeKinds(ctx context.Context, filters mode } return schemaNodeKinds, totalRowCount, nil } - } // GetGraphSchemaNodeKindById - gets a row from the schema_node_kinds table by id. It returns a model.GraphSchemaNodeKind struct populated with the data, or an error if that id does not exist. @@ -463,43 +471,53 @@ func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name strin // populated with data, as well as an integer indicating the total number of rows returned by the query (excluding any given pagination). func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaEdgeKinds, int, error) { var ( - schemaEdgeKinds = model.GraphSchemaEdgeKinds{} - totalRowCount int + schemaEdgeKinds = model.GraphSchemaEdgeKinds{} + totalRowCount int + tableIdentifiedFilters = make(model.Filters, len(edgeKindFilters)) + tableIdentifiedSort = make(model.Sort, len(sort)) ) // add table identifiers to filtering and sorting columns ensuring we don't return an ambiguous column error for column, filters := range edgeKindFilters { if column == "name" { - edgeKindFilters[fmt.Sprintf("%s.%s", kindTable, column)] = filters + tableIdentifiedFilters[fmt.Sprintf("%s.%s", "k", column)] = filters delete(edgeKindFilters, column) } else { - edgeKindFilters[fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), column)] = filters + tableIdentifiedFilters[fmt.Sprintf("%s.%s", "ek", column)] = filters delete(edgeKindFilters, column) } } - for idx, _ := range sort { + for idx, sortItem := range sort { if sort[idx].Column == "name" { - sort[idx].Column = fmt.Sprintf("%s.%s", kindTable, sort[idx].Column) + tableIdentifiedSort[idx] = model.SortItem{ + Direction: sortItem.Direction, + Column: fmt.Sprintf("%s.%s", "k", sort[idx].Column), + } } else { - sort[idx].Column = fmt.Sprintf("%s.%s", model.GraphSchemaEdgeKind{}.TableName(), sort[idx].Column) + tableIdentifiedSort[idx] = model.SortItem{ + Direction: sortItem.Direction, + Column: fmt.Sprintf("%s.%s", "ek", sort[idx].Column), + } } } - if filterAndPagination, err := parseFiltersAndPagination(edgeKindFilters, sort, skip, limit); err != nil { + if filterAndPagination, err := parseFiltersAndPagination(tableIdentifiedFilters, tableIdentifiedSort, skip, limit); err != nil { return schemaEdgeKinds, 0, err } else { - sqlStr := fmt.Sprintf(`SELECT %s.id, %s.name, schema_extension_id, description, is_traversable, created_at, updated_at, deleted_at - FROM %s - JOIN %s ON %s.kind_id = %s.id + 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 %s %s %s`, - model.GraphSchemaEdgeKind{}.TableName(), kindTable, model.GraphSchemaEdgeKind{}.TableName(), kindTable, - model.GraphSchemaEdgeKind{}.TableName(), kindTable, filterAndPagination.WhereClause, filterAndPagination.OrderSql, filterAndPagination.SkipLimit) + model.GraphSchemaEdgeKind{}.TableName(), kindTable, filterAndPagination.WhereClause, + filterAndPagination.OrderSql, filterAndPagination.SkipLimit) if result := s.db.WithContext(ctx).Raw(sqlStr, filterAndPagination.Filter.params...).Scan(&schemaEdgeKinds); result.Error != nil { return nil, 0, CheckError(result) } else { if limit > 0 || skip > 0 { - countSqlStr := fmt.Sprintf(`SELECT COUNT(*) FROM %s %s`, model.GraphSchemaEdgeKind{}.TableName(), filterAndPagination.WhereClause) + countSqlStr := fmt.Sprintf(`SELECT COUNT(*) FROM %s ek JOIN %s k on ek.kind_id = k.id %s`, + model.GraphSchemaEdgeKind{}.TableName(), kindTable, filterAndPagination.WhereClause) if countResult := s.db.WithContext(ctx).Raw(countSqlStr, filterAndPagination.Filter.params...).Scan(&totalRowCount); countResult.Error != nil { return model.GraphSchemaEdgeKinds{}, 0, CheckError(countResult) } @@ -509,7 +527,6 @@ func (s *BloodhoundDB) GetGraphSchemaEdgeKinds(ctx context.Context, edgeKindFilt } return schemaEdgeKinds, totalRowCount, nil } - } func (s *BloodhoundDB) GetGraphSchemaEdgeKindsWithSchemaName(ctx context.Context, edgeKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaEdgeKindsWithNamedSchema, int, error) { diff --git a/cmd/api/src/database/graphschema_integration_test.go b/cmd/api/src/database/graphschema_integration_test.go index 94832972013..5536380d099 100644 --- a/cmd/api/src/database/graphschema_integration_test.go +++ b/cmd/api/src/database/graphschema_integration_test.go @@ -458,7 +458,7 @@ func TestDatabase_GraphSchemaNodeKind_CRUD(t *testing.T) { t.Run("fail - return error for filtering on non-existent column", func(t *testing.T) { _, _, err = testSuite.BHDatabase.GetGraphSchemaNodeKinds(testSuite.Context, model.Filters{"nonexistentcolumn": []model.Filter{{Operator: model.Equals, Value: "blah", SetOperator: model.FilterAnd}}}, model.Sort{}, 0, 0) - require.EqualError(t, err, "ERROR: column \"nonexistentcolumn\" does not exist (SQLSTATE 42703)") + require.EqualError(t, err, "ERROR: column nk.nonexistentcolumn does not exist (SQLSTATE 42703)") }) // UPDATE @@ -896,7 +896,7 @@ func TestDatabase_GraphSchemaEdgeKind_CRUD(t *testing.T) { t.Run("fail - return error for filtering on non-existent column", func(t *testing.T) { _, _, err = testSuite.BHDatabase.GetGraphSchemaEdgeKinds(testSuite.Context, model.Filters{"nonexistentcolumn": []model.Filter{{Operator: model.Equals, Value: "blah", SetOperator: model.FilterAnd}}}, model.Sort{}, 0, 0) - require.EqualError(t, err, "ERROR: column \"nonexistentcolumn\" does not exist (SQLSTATE 42703)") + require.EqualError(t, err, "ERROR: column ek.nonexistentcolumn does not exist (SQLSTATE 42703)") }) // UPDATE From 75a8207e70c01155d40f6caba6a5c303efea1e52 Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:00:04 -0700 Subject: [PATCH 24/27] update parameter test to conform with whats in migration --- cmd/api/src/database/parameters_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/api/src/database/parameters_test.go b/cmd/api/src/database/parameters_test.go index 9081713c5cd..6a539edfb1e 100644 --- a/cmd/api/src/database/parameters_test.go +++ b/cmd/api/src/database/parameters_test.go @@ -176,7 +176,7 @@ func TestParameters_GetAGTParameter(t *testing.T) { require.Equal(t, appcfg.AGTParameters{ DAWGsWorkerLimit: 6, SelectorWorkerLimit: 7, - ExpansionWorkerLimit: 7, + ExpansionWorkerLimit: 3, }, appcfg.GetAGTParameters(testCtx, db)) } From d566a47dfe03870850cb32a280efaa9f4eb8f68f Mon Sep 17 00:00:00 2001 From: Lawson Willard <46655228+LawsonWillard@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:00:31 -0700 Subject: [PATCH 25/27] update node and edge schema tables to use kind id as foriegn key --- cmd/api/src/database/graphschema.go | 64 ++++++++++--------- .../database/graphschema_integration_test.go | 52 --------------- 2 files changed, 33 insertions(+), 83 deletions(-) diff --git a/cmd/api/src/database/graphschema.go b/cmd/api/src/database/graphschema.go index 2f6291b636b..76942d84afb 100644 --- a/cmd/api/src/database/graphschema.go +++ b/cmd/api/src/database/graphschema.go @@ -205,24 +205,25 @@ func (s *BloodhoundDB) DeleteGraphSchemaExtension(ctx context.Context, extension // must also call the DAWGS RefreshKinds function to ensure the kinds are reloaded into the in memory kind map. func (s *BloodhoundDB) CreateGraphSchemaNodeKind(ctx context.Context, name string, extensionId int32, displayName string, description string, isDisplayKind bool, icon, iconColor string) (model.GraphSchemaNodeKind, error) { var schemaNodeKind = model.GraphSchemaNodeKind{} - if db, err := s.db.DB(); err != nil { - return schemaNodeKind, err - } else if row := db.QueryRowContext(ctx, ` - SELECT schema_node_kind_id, return_schema_extension_id, return_name, return_display_name, return_description, - return_is_display_kind, return_icon, return_icon_color, return_created_at, return_updated_at, return_deleted_at - FROM upsert_schema_node_kind($1, $2, $3, $4, $5, $6, $7)`, name, extensionId, displayName, description, isDisplayKind, icon, iconColor); row.Err() != nil { - if strings.Contains(row.Err().Error(), DuplicateKeyValueErrorString) { - return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, row.Err().Error()) - } - return model.GraphSchemaNodeKind{}, row.Err() - } else { - if err := row.Scan(&schemaNodeKind.ID, &schemaNodeKind.SchemaExtensionId, &schemaNodeKind.Name, &schemaNodeKind.DisplayName, - &schemaNodeKind.Description, &schemaNodeKind.IsDisplayKind, &schemaNodeKind.Icon, &schemaNodeKind.IconColor, - &schemaNodeKind.CreatedAt, &schemaNodeKind.UpdatedAt, &schemaNodeKind.DeletedAt); err != nil { - return model.GraphSchemaNodeKind{}, err + if result := s.db.WithContext(ctx).Raw(` + WITH dawgs_kinds + AS ( INSERT INTO kind (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? RETURNING id, name), + inserted_nodes AS ( + INSERT INTO schema_node_kinds (kind_id, schema_extension_id, display_name, description, is_display_kind, icon, icon_color) + SELECT dk.id, ?, ?, ?, ?, ?, ? + FROM dawgs_kinds dk + RETURNING id, kind_id, schema_extension_id, display_name, description, is_display_kind, icon, icon_color, created_at, updated_at, deleted_at + ) + SELECT ins.id, ins.schema_extension_id, dk.name, ins.display_name, ins.description, ins.is_display_kind, ins.icon, ins.icon_color, ins.created_at, ins.updated_at, ins.deleted_at + FROM inserted_nodes ins + JOIN dawgs_kinds dk ON ins.kind_id = dk.id;`, name, name, extensionId, displayName, description, + isDisplayKind, icon, iconColor).Scan(&schemaNodeKind); result.Error != nil { + if strings.Contains(result.Error.Error(), DuplicateKeyValueErrorString) { + return model.GraphSchemaNodeKind{}, fmt.Errorf("%w: %v", ErrDuplicateSchemaNodeKindName, result.Error) } - return schemaNodeKind, nil + return model.GraphSchemaNodeKind{}, result.Error } + return schemaNodeKind, nil } // GetGraphSchemaNodeKinds - returns all rows from the schema_node_kinds table that matches the given model.Filters. It returns a slice of model.GraphSchemaNodeKinds structs @@ -448,23 +449,24 @@ func (s *BloodhoundDB) DeleteGraphSchemaProperty(ctx context.Context, propertyID func (s *BloodhoundDB) CreateGraphSchemaEdgeKind(ctx context.Context, name string, schemaExtensionId int32, description string, isTraversable bool) (model.GraphSchemaEdgeKind, error) { var schemaEdgeKind model.GraphSchemaEdgeKind - if db, err := s.db.DB(); err != nil { - return model.GraphSchemaEdgeKind{}, err - } else if row := db.QueryRowContext(ctx, ` - SELECT schema_edge_kind_id, return_schema_extension_id, return_name, return_description, return_is_traversable, - return_created_at, return_updated_at, return_deleted_at - FROM upsert_schema_edge_kind($1, $2, $3, $4);`, name, schemaExtensionId, description, isTraversable); row.Err() != nil { - if strings.Contains(row.Err().Error(), DuplicateKeyValueErrorString) { - return schemaEdgeKind, fmt.Errorf("%w: %v", ErrDuplicateSchemaEdgeKindName, row.Err().Error()) - } - return schemaEdgeKind, row.Err() - } else { - if err = row.Scan(&schemaEdgeKind.ID, &schemaEdgeKind.SchemaExtensionId, &schemaEdgeKind.Name, &schemaEdgeKind.Description, - &schemaEdgeKind.IsTraversable, &schemaEdgeKind.CreatedAt, &schemaEdgeKind.UpdatedAt, &schemaEdgeKind.DeletedAt); err != nil { - return model.GraphSchemaEdgeKind{}, err + if result := s.db.WithContext(ctx).Raw(` + WITH dawgs_kinds + AS( INSERT INTO kind (name) VALUES (?) ON CONFLICT (name) DO UPDATE SET name = ? RETURNING id, name), + inserted_edges AS ( + INSERT INTO schema_edge_kinds (kind_id, schema_extension_id, description, is_traversable) + SELECT id, ?, ?, ? + FROM dawgs_kinds + 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 + JOIN dawgs_kinds dk ON ie.kind_id = dk.id;`, name, 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) } - return schemaEdgeKind, nil + return schemaEdgeKind, CheckError(result) } + return schemaEdgeKind, nil } // GetGraphSchemaEdgeKinds - returns all rows from the schema_edge_kinds table that matches the given model.Filters. It returns a slice of model.GraphSchemaEdgeKinds diff --git a/cmd/api/src/database/graphschema_integration_test.go b/cmd/api/src/database/graphschema_integration_test.go index 5536380d099..a543239e2d0 100644 --- a/cmd/api/src/database/graphschema_integration_test.go +++ b/cmd/api/src/database/graphschema_integration_test.go @@ -939,58 +939,6 @@ func TestDatabase_GraphSchemaEdgeKind_CRUD(t *testing.T) { }) } -// This test ensures that a schema_node_kind and a schema_edge_kind cannot reference the same DAWGS kind -func TestDatabase_GraphSchemaKind(t *testing.T) { - testSuite := setupIntegrationTestSuite(t) - defer teardownIntegrationTestSuite(t, &testSuite) - extension, err := testSuite.BHDatabase.CreateGraphSchemaExtension(testSuite.Context, "test_extension_schema_edge_kinds", "test_extension", "1.0.0") - require.NoError(t, err) - - var ( - edgeKind1 = model.GraphSchemaEdgeKind{ - SchemaExtensionId: extension.ID, - Name: "Test_Collision", - } - - nodeKind1 = model.GraphSchemaNodeKind{ - Name: "Test_Collision", - SchemaExtensionId: extension.ID, - } - - edgeKind2 = model.GraphSchemaEdgeKind{ - SchemaExtensionId: extension.ID, - Name: "Test_Collision_2", - } - nodeKind2 = model.GraphSchemaNodeKind{ - Name: "Test_Collision_2", - SchemaExtensionId: extension.ID, - } - ) - - t.Run("fail - unable to create schema_node_kind if kind is already declared as an edge kind", func(t *testing.T) { - _, err = testSuite.BHDatabase.CreateGraphSchemaEdgeKind(testSuite.Context, edgeKind1.Name, edgeKind1.SchemaExtensionId, - edgeKind1.Description, edgeKind1.IsTraversable) - require.NoError(t, err) - - _, err = testSuite.BHDatabase.CreateGraphSchemaNodeKind(testSuite.Context, nodeKind1.Name, nodeKind1.SchemaExtensionId, - nodeKind1.DisplayName, nodeKind1.Description, nodeKind1.IsDisplayKind, nodeKind1.Icon, nodeKind1.IconColor) - require.ErrorContainsf(t, err, database.DuplicateKeyValueErrorString, "expected duplicate key value violates unique constraint") - require.ErrorContainsf(t, err, "kind already declared in the schema_edge_kinds table", "expected duplicate key value violates unique constraint") - - }) - - t.Run("fail - unable to create schema_edge_kind if kind is already declared as a node kind", func(t *testing.T) { - _, err = testSuite.BHDatabase.CreateGraphSchemaNodeKind(testSuite.Context, nodeKind2.Name, nodeKind2.SchemaExtensionId, - nodeKind2.DisplayName, nodeKind2.Description, nodeKind2.IsDisplayKind, nodeKind2.Icon, nodeKind2.IconColor) - require.NoError(t, err) - - _, err = testSuite.BHDatabase.CreateGraphSchemaEdgeKind(testSuite.Context, edgeKind2.Name, edgeKind2.SchemaExtensionId, - edgeKind2.Description, edgeKind2.IsTraversable) - require.ErrorContainsf(t, err, database.DuplicateKeyValueErrorString, "expected duplicate key value violates unique constraint") - require.ErrorContainsf(t, err, "kind already declared in the schema_node_kinds table", "expected duplicate key value violates unique constraint") - }) -} - // compareGraphSchemaNodeKinds - compares the returned list of model.GraphSchemaNodeKinds with the expected results. // Since this is used to compare filtered and paginated results ORDER MATTERS for the expected result. func compareGraphSchemaNodeKinds(t *testing.T, got, want model.GraphSchemaNodeKinds) { From 787563019589977625b8d1a620607950d7bbac7a Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:58:10 -0500 Subject: [PATCH 26/27] BED-6721: update for BED-7067 --- .../migration/extensions/ad_graph_schema.sql | 208 +++++++++--------- .../migration/extensions/az_graph_schema.sql | 142 ++++++------ packages/go/schemagen/generator/sql.go | 10 +- 3 files changed, 180 insertions(+), 180 deletions(-) diff --git a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql index 55a4eef09ac..3beb886bbf4 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -128,109 +128,109 @@ DECLARE BEGIN INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true) RETURNING id INTO new_extension_id; - INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES - (new_extension_id, 'Base', 'Entity', '', false, '', ''), - (new_extension_id, 'User', 'User', '', true, 'fa-user', '#17E625'), - (new_extension_id, 'Computer', 'Computer', '', true, 'fa-desktop', '#E67873'), - (new_extension_id, 'Group', 'Group', '', true, 'fa-users', '#DBE617'), - (new_extension_id, 'GPO', 'GPO', '', true, 'fa-list', '#998EFD'), - (new_extension_id, 'OU', 'OU', '', true, 'fa-sitemap', '#FFAA00'), - (new_extension_id, 'Container', 'Container', '', true, 'fa-box', '#F79A78'), - (new_extension_id, 'Domain', 'Domain', '', true, 'fa-globe', '#17E6B9'), - (new_extension_id, 'ADLocalGroup', 'LocalGroup', '', false, '', ''), - (new_extension_id, 'ADLocalUser', 'LocalUser', '', false, '', ''), - (new_extension_id, 'AIACA', 'AIACA', '', true, 'fa-arrows-left-right-to-line', '#9769F0'), - (new_extension_id, 'RootCA', 'RootCA', '', true, 'fa-landmark', '#6968E8'), - (new_extension_id, 'EnterpriseCA', 'EnterpriseCA', '', true, 'fa-building', '#4696E9'), - (new_extension_id, 'NTAuthStore', 'NTAuthStore', '', true, 'fa-store', '#D575F5'), - (new_extension_id, 'CertTemplate', 'CertTemplate', '', true, 'fa-id-card', '#B153F3'), - (new_extension_id, 'IssuancePolicy', 'IssuancePolicy', '', true, 'fa-clipboard-check', '#99B2DD'); + INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES + (new_extension_id, (SELECT id FROM kind WHERE name = 'Base'), 'Entity', '', false, '', ''), + (new_extension_id, (SELECT id FROM kind WHERE name = 'User'), 'User', '', true, 'fa-user', '#17E625'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'Computer'), 'Computer', '', true, 'fa-desktop', '#E67873'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'Group'), 'Group', '', true, 'fa-users', '#DBE617'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GPO'), 'GPO', '', true, 'fa-list', '#998EFD'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'OU'), 'OU', '', true, 'fa-sitemap', '#FFAA00'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'Container'), 'Container', '', true, 'fa-box', '#F79A78'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'Domain'), 'Domain', '', true, 'fa-globe', '#17E6B9'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADLocalGroup'), 'LocalGroup', '', false, '', ''), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADLocalUser'), 'LocalUser', '', false, '', ''), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AIACA'), 'AIACA', '', true, 'fa-arrows-left-right-to-line', '#9769F0'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'RootCA'), 'RootCA', '', true, 'fa-landmark', '#6968E8'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'EnterpriseCA'), 'EnterpriseCA', '', true, 'fa-building', '#4696E9'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'NTAuthStore'), 'NTAuthStore', '', true, 'fa-store', '#D575F5'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CertTemplate'), 'CertTemplate', '', true, 'fa-id-card', '#B153F3'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'IssuancePolicy'), 'IssuancePolicy', '', true, 'fa-clipboard-check', '#99B2DD'); - INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES - (new_extension_id, 'Owns', '', true), - (new_extension_id, 'GenericAll', '', true), - (new_extension_id, 'GenericWrite', '', true), - (new_extension_id, 'WriteOwner', '', true), - (new_extension_id, 'WriteDacl', '', true), - (new_extension_id, 'MemberOf', '', true), - (new_extension_id, 'ForceChangePassword', '', true), - (new_extension_id, 'AllExtendedRights', '', true), - (new_extension_id, 'AddMember', '', true), - (new_extension_id, 'HasSession', '', true), - (new_extension_id, 'Contains', '', true), - (new_extension_id, 'GPLink', '', true), - (new_extension_id, 'AllowedToDelegate', '', true), - (new_extension_id, 'CoerceToTGT', '', true), - (new_extension_id, 'GetChanges', '', false), - (new_extension_id, 'GetChangesAll', '', false), - (new_extension_id, 'GetChangesInFilteredSet', '', false), - (new_extension_id, 'CrossForestTrust', '', false), - (new_extension_id, 'SameForestTrust', '', true), - (new_extension_id, 'SpoofSIDHistory', '', true), - (new_extension_id, 'AbuseTGTDelegation', '', true), - (new_extension_id, 'AllowedToAct', '', true), - (new_extension_id, 'AdminTo', '', true), - (new_extension_id, 'CanPSRemote', '', true), - (new_extension_id, 'CanRDP', '', true), - (new_extension_id, 'ExecuteDCOM', '', true), - (new_extension_id, 'HasSIDHistory', '', true), - (new_extension_id, 'AddSelf', '', true), - (new_extension_id, 'DCSync', '', true), - (new_extension_id, 'ReadLAPSPassword', '', true), - (new_extension_id, 'ReadGMSAPassword', '', true), - (new_extension_id, 'DumpSMSAPassword', '', true), - (new_extension_id, 'SQLAdmin', '', true), - (new_extension_id, 'AddAllowedToAct', '', true), - (new_extension_id, 'WriteSPN', '', true), - (new_extension_id, 'AddKeyCredentialLink', '', true), - (new_extension_id, 'LocalToComputer', '', false), - (new_extension_id, 'MemberOfLocalGroup', '', false), - (new_extension_id, 'RemoteInteractiveLogonRight', '', false), - (new_extension_id, 'SyncLAPSPassword', '', true), - (new_extension_id, 'WriteAccountRestrictions', '', true), - (new_extension_id, 'WriteGPLink', '', true), - (new_extension_id, 'RootCAFor', '', false), - (new_extension_id, 'DCFor', '', true), - (new_extension_id, 'PublishedTo', '', false), - (new_extension_id, 'ManageCertificates', '', true), - (new_extension_id, 'ManageCA', '', true), - (new_extension_id, 'DelegatedEnrollmentAgent', '', false), - (new_extension_id, 'Enroll', '', false), - (new_extension_id, 'HostsCAService', '', false), - (new_extension_id, 'WritePKIEnrollmentFlag', '', false), - (new_extension_id, 'WritePKINameFlag', '', false), - (new_extension_id, 'NTAuthStoreFor', '', false), - (new_extension_id, 'TrustedForNTAuth', '', false), - (new_extension_id, 'EnterpriseCAFor', '', false), - (new_extension_id, 'IssuedSignedBy', '', false), - (new_extension_id, 'GoldenCert', '', true), - (new_extension_id, 'EnrollOnBehalfOf', '', false), - (new_extension_id, 'OIDGroupLink', '', false), - (new_extension_id, 'ExtendedByPolicy', '', false), - (new_extension_id, 'ADCSESC1', '', true), - (new_extension_id, 'ADCSESC3', '', true), - (new_extension_id, 'ADCSESC4', '', true), - (new_extension_id, 'ADCSESC6a', '', true), - (new_extension_id, 'ADCSESC6b', '', true), - (new_extension_id, 'ADCSESC9a', '', true), - (new_extension_id, 'ADCSESC9b', '', true), - (new_extension_id, 'ADCSESC10a', '', true), - (new_extension_id, 'ADCSESC10b', '', true), - (new_extension_id, 'ADCSESC13', '', true), - (new_extension_id, 'SyncedToEntraUser', '', true), - (new_extension_id, 'CoerceAndRelayNTLMToSMB', '', true), - (new_extension_id, 'CoerceAndRelayNTLMToADCS', '', true), - (new_extension_id, 'WriteOwnerLimitedRights', '', true), - (new_extension_id, 'WriteOwnerRaw', '', false), - (new_extension_id, 'OwnsLimitedRights', '', true), - (new_extension_id, 'OwnsRaw', '', false), - (new_extension_id, 'ClaimSpecialIdentity', '', true), - (new_extension_id, 'CoerceAndRelayNTLMToLDAP', '', true), - (new_extension_id, 'CoerceAndRelayNTLMToLDAPS', '', true), - (new_extension_id, 'ContainsIdentity', '', true), - (new_extension_id, 'PropagatesACEsTo', '', true), - (new_extension_id, 'GPOAppliesTo', '', true), - (new_extension_id, 'CanApplyGPO', '', true), - (new_extension_id, 'HasTrustKeys', '', true), - (new_extension_id, 'ProtectAdminGroups', '', false); + INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES + (new_extension_id, (SELECT id FROM kind WHERE name = 'Owns'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GenericAll'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GenericWrite'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteOwner'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteDacl'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'MemberOf'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ForceChangePassword'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AllExtendedRights'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AddMember'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'HasSession'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'Contains'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GPLink'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AllowedToDelegate'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceToTGT'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GetChanges'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GetChangesAll'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GetChangesInFilteredSet'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CrossForestTrust'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'SameForestTrust'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'SpoofSIDHistory'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AbuseTGTDelegation'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AllowedToAct'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AdminTo'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CanPSRemote'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CanRDP'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ExecuteDCOM'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'HasSIDHistory'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AddSelf'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'DCSync'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ReadLAPSPassword'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ReadGMSAPassword'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'DumpSMSAPassword'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'SQLAdmin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AddAllowedToAct'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteSPN'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AddKeyCredentialLink'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'LocalToComputer'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'MemberOfLocalGroup'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'RemoteInteractiveLogonRight'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'SyncLAPSPassword'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteAccountRestrictions'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteGPLink'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'RootCAFor'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'DCFor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'PublishedTo'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ManageCertificates'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ManageCA'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'DelegatedEnrollmentAgent'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'Enroll'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'HostsCAService'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WritePKIEnrollmentFlag'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WritePKINameFlag'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'NTAuthStoreFor'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'TrustedForNTAuth'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'EnterpriseCAFor'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'IssuedSignedBy'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GoldenCert'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'EnrollOnBehalfOf'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'OIDGroupLink'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ExtendedByPolicy'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC1'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC3'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC4'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC6a'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC6b'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC9a'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC9b'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC10a'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC10b'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC13'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'SyncedToEntraUser'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToSMB'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToADCS'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteOwnerLimitedRights'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteOwnerRaw'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'OwnsLimitedRights'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'OwnsRaw'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ClaimSpecialIdentity'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToLDAP'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToLDAPS'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ContainsIdentity'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'PropagatesACEsTo'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'GPOAppliesTo'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'CanApplyGPO'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'HasTrustKeys'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'ProtectAdminGroups'), '', false); END $$; diff --git a/cmd/api/src/database/migration/extensions/az_graph_schema.sql b/cmd/api/src/database/migration/extensions/az_graph_schema.sql index f5654333df5..ace4c5a72db 100644 --- a/cmd/api/src/database/migration/extensions/az_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/az_graph_schema.sql @@ -95,76 +95,76 @@ DECLARE BEGIN INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true) RETURNING id INTO new_extension_id; - INSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES - (new_extension_id, 'AZBase', 'Entity', '', false, '', ''), - (new_extension_id, 'AZVMScaleSet', 'VMScaleSet', '', true, 'fa-server', '#007CD0'), - (new_extension_id, 'AZApp', 'App', '', true, 'fa-window-restore', '#03FC84'), - (new_extension_id, 'AZRole', 'Role', '', true, 'fa-clipboard-list', '#ED8537'), - (new_extension_id, 'AZDevice', 'Device', '', true, 'fa-desktop', '#B18FCF'), - (new_extension_id, 'AZFunctionApp', 'FunctionApp', '', true, 'fa-bolt', '#F4BA44'), - (new_extension_id, 'AZGroup', 'Group', '', true, 'fa-users', '#F57C9B'), - (new_extension_id, 'AZKeyVault', 'KeyVault', '', true, 'fa-lock', '#ED658C'), - (new_extension_id, 'AZManagementGroup', 'ManagementGroup', '', true, 'fa-sitemap', '#BD93D8'), - (new_extension_id, 'AZResourceGroup', 'ResourceGroup', '', true, 'fa-cube', '#89BD9E'), - (new_extension_id, 'AZServicePrincipal', 'ServicePrincipal', '', true, 'fa-robot', '#C1D6D6'), - (new_extension_id, 'AZSubscription', 'Subscription', '', true, 'fa-key', '#D2CCA1'), - (new_extension_id, 'AZTenant', 'Tenant', '', true, 'fa-cloud', '#54F2F2'), - (new_extension_id, 'AZUser', 'User', '', true, 'fa-user', '#34D2EB'), - (new_extension_id, 'AZVM', 'VM', '', true, 'fa-desktop', '#F9ADA0'), - (new_extension_id, 'AZManagedCluster', 'ManagedCluster', '', true, 'fa-cubes', '#326CE5'), - (new_extension_id, 'AZContainerRegistry', 'ContainerRegistry', '', true, 'fa-box-open', '#0885D7'), - (new_extension_id, 'AZWebApp', 'WebApp', '', true, 'fa-object-group', '#4696E9'), - (new_extension_id, 'AZLogicApp', 'LogicApp', '', true, 'fa-sitemap', '#9EE047'), - (new_extension_id, 'AZAutomationAccount', 'AutomationAccount', '', true, 'fa-cog', '#F4BA44'); + INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZBase'), 'Entity', '', false, '', ''), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVMScaleSet'), 'VMScaleSet', '', true, 'fa-server', '#007CD0'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZApp'), 'App', '', true, 'fa-window-restore', '#03FC84'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRole'), 'Role', '', true, 'fa-clipboard-list', '#ED8537'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZDevice'), 'Device', '', true, 'fa-desktop', '#B18FCF'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZFunctionApp'), 'FunctionApp', '', true, 'fa-bolt', '#F4BA44'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGroup'), 'Group', '', true, 'fa-users', '#F57C9B'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZKeyVault'), 'KeyVault', '', true, 'fa-lock', '#ED658C'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZManagementGroup'), 'ManagementGroup', '', true, 'fa-sitemap', '#BD93D8'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZResourceGroup'), 'ResourceGroup', '', true, 'fa-cube', '#89BD9E'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZServicePrincipal'), 'ServicePrincipal', '', true, 'fa-robot', '#C1D6D6'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZSubscription'), 'Subscription', '', true, 'fa-key', '#D2CCA1'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZTenant'), 'Tenant', '', true, 'fa-cloud', '#54F2F2'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZUser'), 'User', '', true, 'fa-user', '#34D2EB'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVM'), 'VM', '', true, 'fa-desktop', '#F9ADA0'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZManagedCluster'), 'ManagedCluster', '', true, 'fa-cubes', '#326CE5'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZContainerRegistry'), 'ContainerRegistry', '', true, 'fa-box-open', '#0885D7'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZWebApp'), 'WebApp', '', true, 'fa-object-group', '#4696E9'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZLogicApp'), 'LogicApp', '', true, 'fa-sitemap', '#9EE047'), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAutomationAccount'), 'AutomationAccount', '', true, 'fa-cog', '#F4BA44'); - INSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES - (new_extension_id, 'AZAvereContributor', '', true), - (new_extension_id, 'AZContains', '', true), - (new_extension_id, 'AZContributor', '', true), - (new_extension_id, 'AZGetCertificates', '', true), - (new_extension_id, 'AZGetKeys', '', true), - (new_extension_id, 'AZGetSecrets', '', true), - (new_extension_id, 'AZHasRole', '', true), - (new_extension_id, 'AZMemberOf', '', true), - (new_extension_id, 'AZOwner', '', true), - (new_extension_id, 'AZRunsAs', '', true), - (new_extension_id, 'AZVMContributor', '', true), - (new_extension_id, 'AZAutomationContributor', '', true), - (new_extension_id, 'AZKeyVaultContributor', '', true), - (new_extension_id, 'AZVMAdminLogin', '', true), - (new_extension_id, 'AZAddMembers', '', true), - (new_extension_id, 'AZAddSecret', '', true), - (new_extension_id, 'AZExecuteCommand', '', true), - (new_extension_id, 'AZGlobalAdmin', '', true), - (new_extension_id, 'AZPrivilegedAuthAdmin', '', true), - (new_extension_id, 'AZGrant', '', true), - (new_extension_id, 'AZGrantSelf', '', true), - (new_extension_id, 'AZPrivilegedRoleAdmin', '', true), - (new_extension_id, 'AZResetPassword', '', true), - (new_extension_id, 'AZUserAccessAdministrator', '', true), - (new_extension_id, 'AZOwns', '', true), - (new_extension_id, 'AZScopedTo', '', false), - (new_extension_id, 'AZCloudAppAdmin', '', true), - (new_extension_id, 'AZAppAdmin', '', true), - (new_extension_id, 'AZAddOwner', '', true), - (new_extension_id, 'AZManagedIdentity', '', true), - (new_extension_id, 'AZMGApplication_ReadWrite_All', '', false), - (new_extension_id, 'AZMGAppRoleAssignment_ReadWrite_All', '', false), - (new_extension_id, 'AZMGDirectory_ReadWrite_All', '', false), - (new_extension_id, 'AZMGGroup_ReadWrite_All', '', false), - (new_extension_id, 'AZMGGroupMember_ReadWrite_All', '', false), - (new_extension_id, 'AZMGRoleManagement_ReadWrite_Directory', '', false), - (new_extension_id, 'AZMGServicePrincipalEndpoint_ReadWrite_All', '', false), - (new_extension_id, 'AZAKSContributor', '', true), - (new_extension_id, 'AZNodeResourceGroup', '', true), - (new_extension_id, 'AZWebsiteContributor', '', true), - (new_extension_id, 'AZLogicAppContributor', '', true), - (new_extension_id, 'AZMGAddMember', '', true), - (new_extension_id, 'AZMGAddOwner', '', true), - (new_extension_id, 'AZMGAddSecret', '', true), - (new_extension_id, 'AZMGGrantAppRoles', '', true), - (new_extension_id, 'AZMGGrantRole', '', true), - (new_extension_id, 'SyncedToADUser', '', true), - (new_extension_id, 'AZRoleEligible', '', true), - (new_extension_id, 'AZRoleApprover', '', true); + INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAvereContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZContains'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGetCertificates'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGetKeys'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGetSecrets'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZHasRole'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMemberOf'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZOwner'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRunsAs'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVMContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAutomationContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZKeyVaultContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVMAdminLogin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAddMembers'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAddSecret'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZExecuteCommand'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGlobalAdmin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZPrivilegedAuthAdmin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGrant'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGrantSelf'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZPrivilegedRoleAdmin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZResetPassword'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZUserAccessAdministrator'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZOwns'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZScopedTo'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZCloudAppAdmin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAppAdmin'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAddOwner'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZManagedIdentity'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGApplication_ReadWrite_All'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAppRoleAssignment_ReadWrite_All'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGDirectory_ReadWrite_All'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGroup_ReadWrite_All'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGroupMember_ReadWrite_All'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGRoleManagement_ReadWrite_Directory'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGServicePrincipalEndpoint_ReadWrite_All'), '', false), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAKSContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZNodeResourceGroup'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZWebsiteContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZLogicAppContributor'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAddMember'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAddOwner'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAddSecret'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGrantAppRoles'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGrantRole'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'SyncedToADUser'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRoleEligible'), '', true), + (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRoleApprover'), '', true); END $$; diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 234b50f8b74..0dfdc354a77 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -197,13 +197,13 @@ func GenerateExtensionSQL(name string, displayName string, version string, dir s sb.WriteString(fmt.Sprintf("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('%s', '%s', '%s', true) RETURNING id INTO new_extension_id;\n\n", name, displayName, version)) - sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, name, display_name, description, is_display_kind, icon, icon_color) VALUES\n") + sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES\n") for i, kind := range nodeKinds { if iconInfo, found := nodeIcons[kind.GetRepresentation()]; found { - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, (SELECT id FROM kind WHERE name = '%s'), '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) } else { - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, (SELECT id FROM kind WHERE name = '%s'), '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) } if i != len(nodeKinds)-1 { @@ -213,7 +213,7 @@ func GenerateExtensionSQL(name string, displayName string, version string, dir s sb.WriteString(";\n\n") - sb.WriteString("\tINSERT INTO schema_edge_kinds (schema_extension_id, name, description, is_traversable) VALUES\n") + sb.WriteString("\tINSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES\n") traversableMap := make(map[string]struct{}) @@ -224,7 +224,7 @@ func GenerateExtensionSQL(name string, displayName string, version string, dir s for i, kind := range relationshipKinds { _, traversable := traversableMap[kind.GetRepresentation()] - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, '%s', '', %t)", kind.GetRepresentation(), traversable)) + sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, (SELECT id FROM kind WHERE name = '%s'), '', %t)", kind.GetRepresentation(), traversable)) if i != len(relationshipKinds)-1 { sb.WriteString(",\n") From 3a481bb5370e9198c9f9490d9c4f11d9e6828420 Mon Sep 17 00:00:00 2001 From: Wes <169498386+wes-mil@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:30:22 -0500 Subject: [PATCH 27/27] BED-6721: this is the worst code i have ever written --- .../migration/extensions/ad_graph_schema.sql | 470 ++++++++++-------- .../migration/extensions/az_graph_schema.sql | 338 +++++++------ packages/go/schemagen/generator/sql.go | 109 ++-- 3 files changed, 524 insertions(+), 393 deletions(-) diff --git a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql index 3beb886bbf4..b226693e513 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -15,222 +15,270 @@ -- SPDX-License-Identifier: Apache-2.0 -- Code generated by Cuelang code gen. DO NOT EDIT! -- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ -INSERT INTO kind (name) VALUES - ('Base'), - ('User'), - ('Computer'), - ('Group'), - ('GPO'), - ('OU'), - ('Container'), - ('Domain'), - ('ADLocalGroup'), - ('ADLocalUser'), - ('AIACA'), - ('RootCA'), - ('EnterpriseCA'), - ('NTAuthStore'), - ('CertTemplate'), - ('IssuancePolicy'), - ('Owns'), - ('GenericAll'), - ('GenericWrite'), - ('WriteOwner'), - ('WriteDacl'), - ('MemberOf'), - ('ForceChangePassword'), - ('AllExtendedRights'), - ('AddMember'), - ('HasSession'), - ('Contains'), - ('GPLink'), - ('AllowedToDelegate'), - ('CoerceToTGT'), - ('GetChanges'), - ('GetChangesAll'), - ('GetChangesInFilteredSet'), - ('CrossForestTrust'), - ('SameForestTrust'), - ('SpoofSIDHistory'), - ('AbuseTGTDelegation'), - ('AllowedToAct'), - ('AdminTo'), - ('CanPSRemote'), - ('CanRDP'), - ('ExecuteDCOM'), - ('HasSIDHistory'), - ('AddSelf'), - ('DCSync'), - ('ReadLAPSPassword'), - ('ReadGMSAPassword'), - ('DumpSMSAPassword'), - ('SQLAdmin'), - ('AddAllowedToAct'), - ('WriteSPN'), - ('AddKeyCredentialLink'), - ('LocalToComputer'), - ('MemberOfLocalGroup'), - ('RemoteInteractiveLogonRight'), - ('SyncLAPSPassword'), - ('WriteAccountRestrictions'), - ('WriteGPLink'), - ('RootCAFor'), - ('DCFor'), - ('PublishedTo'), - ('ManageCertificates'), - ('ManageCA'), - ('DelegatedEnrollmentAgent'), - ('Enroll'), - ('HostsCAService'), - ('WritePKIEnrollmentFlag'), - ('WritePKINameFlag'), - ('NTAuthStoreFor'), - ('TrustedForNTAuth'), - ('EnterpriseCAFor'), - ('IssuedSignedBy'), - ('GoldenCert'), - ('EnrollOnBehalfOf'), - ('OIDGroupLink'), - ('ExtendedByPolicy'), - ('ADCSESC1'), - ('ADCSESC3'), - ('ADCSESC4'), - ('ADCSESC6a'), - ('ADCSESC6b'), - ('ADCSESC9a'), - ('ADCSESC9b'), - ('ADCSESC10a'), - ('ADCSESC10b'), - ('ADCSESC13'), - ('SyncedToEntraUser'), - ('CoerceAndRelayNTLMToSMB'), - ('CoerceAndRelayNTLMToADCS'), - ('WriteOwnerLimitedRights'), - ('WriteOwnerRaw'), - ('OwnsLimitedRights'), - ('OwnsRaw'), - ('ClaimSpecialIdentity'), - ('CoerceAndRelayNTLMToLDAP'), - ('CoerceAndRelayNTLMToLDAPS'), - ('ContainsIdentity'), - ('PropagatesACEsTo'), - ('GPOAppliesTo'), - ('CanApplyGPO'), - ('HasTrustKeys'), - ('ProtectAdminGroups') -ON CONFLICT (name) DO NOTHING; +CREATE OR REPLACE FUNCTION genscript_upsert_kind(node_kind_name TEXT) RETURNS void AS $$ +BEGIN + IF NOT EXISTS (SELECT id FROM kind WHERE kind.name = node_kind_name) THEN + INSERT INTO kind (name) VALUES (node_kind_name); + END IF; +END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION genscript_upsert_schema_node_kind(v_extension_id INT, v_kind_name VARCHAR(256), v_display_name TEXT, v_description TEXT, v_is_display_kind BOOLEAN, v_icon TEXT, v_icon_color TEXT) RETURNS void AS $$ +DECLARE + upserted_kind_id SMALLINT; +BEGIN + SELECT id INTO upserted_kind_id FROM kind WHERE name = v_kind_name; + IF upserted_kind_id IS NULL THEN + RAISE EXCEPTION 'no kind name'; + END IF; + + IF NOT EXISTS (SELECT id FROM schema_node_kinds nk WHERE nk.kind_id = upserted_kind_id) THEN + INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES (v_extension_id, upserted_kind_id, v_display_name, v_description, v_is_display_kind, v_icon, v_icon_color); + ELSE + UPDATE schema_node_kinds SET display_name = v_display_name, description = v_description, is_display_kind = v_is_display_kind, icon = v_icon, icon_color = v_icon_color WHERE kind_id = upserted_kind_id; + END IF; +END; +$$ LANGUAGE plpgsql; -DELETE FROM schema_extensions WHERE name = 'AD'; +CREATE OR REPLACE FUNCTION genscript_upsert_schema_edge_kind(v_extension_id INT, v_kind_name VARCHAR(256), v_description TEXT, v_is_traversable BOOLEAN) RETURNS void AS $$ +DECLARE + upserted_kind_id SMALLINT; +BEGIN + SELECT id INTO upserted_kind_id FROM kind WHERE name = v_kind_name; + IF upserted_kind_id IS NULL THEN + RAISE EXCEPTION 'no kind name'; + END IF; + + IF NOT EXISTS (SELECT id FROM schema_edge_kinds ek WHERE ek.kind_id = upserted_kind_id) THEN + INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES (v_extension_id, upserted_kind_id, v_description, v_is_traversable); + ELSE + UPDATE schema_edge_kinds SET description = v_description, is_traversable = v_is_traversable WHERE kind_id = upserted_kind_id; + END IF; +END; +$$ LANGUAGE plpgsql; DO $$ DECLARE - new_extension_id INT; + extension_id INT; BEGIN - INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true) RETURNING id INTO new_extension_id; + LOCK schema_extensions, schema_node_kinds, schema_edge_kinds, kind; - INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES - (new_extension_id, (SELECT id FROM kind WHERE name = 'Base'), 'Entity', '', false, '', ''), - (new_extension_id, (SELECT id FROM kind WHERE name = 'User'), 'User', '', true, 'fa-user', '#17E625'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'Computer'), 'Computer', '', true, 'fa-desktop', '#E67873'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'Group'), 'Group', '', true, 'fa-users', '#DBE617'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GPO'), 'GPO', '', true, 'fa-list', '#998EFD'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'OU'), 'OU', '', true, 'fa-sitemap', '#FFAA00'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'Container'), 'Container', '', true, 'fa-box', '#F79A78'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'Domain'), 'Domain', '', true, 'fa-globe', '#17E6B9'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADLocalGroup'), 'LocalGroup', '', false, '', ''), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADLocalUser'), 'LocalUser', '', false, '', ''), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AIACA'), 'AIACA', '', true, 'fa-arrows-left-right-to-line', '#9769F0'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'RootCA'), 'RootCA', '', true, 'fa-landmark', '#6968E8'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'EnterpriseCA'), 'EnterpriseCA', '', true, 'fa-building', '#4696E9'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'NTAuthStore'), 'NTAuthStore', '', true, 'fa-store', '#D575F5'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CertTemplate'), 'CertTemplate', '', true, 'fa-id-card', '#B153F3'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'IssuancePolicy'), 'IssuancePolicy', '', true, 'fa-clipboard-check', '#99B2DD'); + IF NOT EXISTS (SELECT id FROM schema_extensions WHERE name = 'AD') THEN + INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AD', 'Active Directory', 'v0.0.1', true) RETURNING id INTO extension_id; + ELSE + UPDATE schema_extensions SET display_name = 'Active Directory', version = 'v0.0.1' WHERE name = 'AD' RETURNING id INTO extension_id; + END IF; - INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES - (new_extension_id, (SELECT id FROM kind WHERE name = 'Owns'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GenericAll'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GenericWrite'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteOwner'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteDacl'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'MemberOf'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ForceChangePassword'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AllExtendedRights'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AddMember'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'HasSession'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'Contains'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GPLink'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AllowedToDelegate'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceToTGT'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GetChanges'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GetChangesAll'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GetChangesInFilteredSet'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CrossForestTrust'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'SameForestTrust'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'SpoofSIDHistory'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AbuseTGTDelegation'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AllowedToAct'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AdminTo'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CanPSRemote'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CanRDP'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ExecuteDCOM'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'HasSIDHistory'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AddSelf'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'DCSync'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ReadLAPSPassword'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ReadGMSAPassword'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'DumpSMSAPassword'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'SQLAdmin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AddAllowedToAct'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteSPN'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AddKeyCredentialLink'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'LocalToComputer'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'MemberOfLocalGroup'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'RemoteInteractiveLogonRight'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'SyncLAPSPassword'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteAccountRestrictions'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteGPLink'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'RootCAFor'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'DCFor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'PublishedTo'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ManageCertificates'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ManageCA'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'DelegatedEnrollmentAgent'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'Enroll'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'HostsCAService'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WritePKIEnrollmentFlag'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WritePKINameFlag'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'NTAuthStoreFor'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'TrustedForNTAuth'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'EnterpriseCAFor'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'IssuedSignedBy'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GoldenCert'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'EnrollOnBehalfOf'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'OIDGroupLink'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ExtendedByPolicy'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC1'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC3'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC4'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC6a'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC6b'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC9a'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC9b'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC10a'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC10b'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ADCSESC13'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'SyncedToEntraUser'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToSMB'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToADCS'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteOwnerLimitedRights'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'WriteOwnerRaw'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'OwnsLimitedRights'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'OwnsRaw'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ClaimSpecialIdentity'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToLDAP'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CoerceAndRelayNTLMToLDAPS'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ContainsIdentity'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'PropagatesACEsTo'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'GPOAppliesTo'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'CanApplyGPO'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'HasTrustKeys'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'ProtectAdminGroups'), '', false); + -- Insert Node Kinds + PERFORM genscript_upsert_kind('Base'); + PERFORM genscript_upsert_kind('User'); + PERFORM genscript_upsert_kind('Computer'); + PERFORM genscript_upsert_kind('Group'); + PERFORM genscript_upsert_kind('GPO'); + PERFORM genscript_upsert_kind('OU'); + PERFORM genscript_upsert_kind('Container'); + PERFORM genscript_upsert_kind('Domain'); + PERFORM genscript_upsert_kind('ADLocalGroup'); + PERFORM genscript_upsert_kind('ADLocalUser'); + PERFORM genscript_upsert_kind('AIACA'); + PERFORM genscript_upsert_kind('RootCA'); + PERFORM genscript_upsert_kind('EnterpriseCA'); + PERFORM genscript_upsert_kind('NTAuthStore'); + PERFORM genscript_upsert_kind('CertTemplate'); + PERFORM genscript_upsert_kind('IssuancePolicy'); + + -- Insert Relationship Kinds + PERFORM genscript_upsert_kind('Owns'); + PERFORM genscript_upsert_kind('GenericAll'); + PERFORM genscript_upsert_kind('GenericWrite'); + PERFORM genscript_upsert_kind('WriteOwner'); + PERFORM genscript_upsert_kind('WriteDacl'); + PERFORM genscript_upsert_kind('MemberOf'); + PERFORM genscript_upsert_kind('ForceChangePassword'); + PERFORM genscript_upsert_kind('AllExtendedRights'); + PERFORM genscript_upsert_kind('AddMember'); + PERFORM genscript_upsert_kind('HasSession'); + PERFORM genscript_upsert_kind('Contains'); + PERFORM genscript_upsert_kind('GPLink'); + PERFORM genscript_upsert_kind('AllowedToDelegate'); + PERFORM genscript_upsert_kind('CoerceToTGT'); + PERFORM genscript_upsert_kind('GetChanges'); + PERFORM genscript_upsert_kind('GetChangesAll'); + PERFORM genscript_upsert_kind('GetChangesInFilteredSet'); + PERFORM genscript_upsert_kind('CrossForestTrust'); + PERFORM genscript_upsert_kind('SameForestTrust'); + PERFORM genscript_upsert_kind('SpoofSIDHistory'); + PERFORM genscript_upsert_kind('AbuseTGTDelegation'); + PERFORM genscript_upsert_kind('AllowedToAct'); + PERFORM genscript_upsert_kind('AdminTo'); + PERFORM genscript_upsert_kind('CanPSRemote'); + PERFORM genscript_upsert_kind('CanRDP'); + PERFORM genscript_upsert_kind('ExecuteDCOM'); + PERFORM genscript_upsert_kind('HasSIDHistory'); + PERFORM genscript_upsert_kind('AddSelf'); + PERFORM genscript_upsert_kind('DCSync'); + PERFORM genscript_upsert_kind('ReadLAPSPassword'); + PERFORM genscript_upsert_kind('ReadGMSAPassword'); + PERFORM genscript_upsert_kind('DumpSMSAPassword'); + PERFORM genscript_upsert_kind('SQLAdmin'); + PERFORM genscript_upsert_kind('AddAllowedToAct'); + PERFORM genscript_upsert_kind('WriteSPN'); + PERFORM genscript_upsert_kind('AddKeyCredentialLink'); + PERFORM genscript_upsert_kind('LocalToComputer'); + PERFORM genscript_upsert_kind('MemberOfLocalGroup'); + PERFORM genscript_upsert_kind('RemoteInteractiveLogonRight'); + PERFORM genscript_upsert_kind('SyncLAPSPassword'); + PERFORM genscript_upsert_kind('WriteAccountRestrictions'); + PERFORM genscript_upsert_kind('WriteGPLink'); + PERFORM genscript_upsert_kind('RootCAFor'); + PERFORM genscript_upsert_kind('DCFor'); + PERFORM genscript_upsert_kind('PublishedTo'); + PERFORM genscript_upsert_kind('ManageCertificates'); + PERFORM genscript_upsert_kind('ManageCA'); + PERFORM genscript_upsert_kind('DelegatedEnrollmentAgent'); + PERFORM genscript_upsert_kind('Enroll'); + PERFORM genscript_upsert_kind('HostsCAService'); + PERFORM genscript_upsert_kind('WritePKIEnrollmentFlag'); + PERFORM genscript_upsert_kind('WritePKINameFlag'); + PERFORM genscript_upsert_kind('NTAuthStoreFor'); + PERFORM genscript_upsert_kind('TrustedForNTAuth'); + PERFORM genscript_upsert_kind('EnterpriseCAFor'); + PERFORM genscript_upsert_kind('IssuedSignedBy'); + PERFORM genscript_upsert_kind('GoldenCert'); + PERFORM genscript_upsert_kind('EnrollOnBehalfOf'); + PERFORM genscript_upsert_kind('OIDGroupLink'); + PERFORM genscript_upsert_kind('ExtendedByPolicy'); + PERFORM genscript_upsert_kind('ADCSESC1'); + PERFORM genscript_upsert_kind('ADCSESC3'); + PERFORM genscript_upsert_kind('ADCSESC4'); + PERFORM genscript_upsert_kind('ADCSESC6a'); + PERFORM genscript_upsert_kind('ADCSESC6b'); + PERFORM genscript_upsert_kind('ADCSESC9a'); + PERFORM genscript_upsert_kind('ADCSESC9b'); + PERFORM genscript_upsert_kind('ADCSESC10a'); + PERFORM genscript_upsert_kind('ADCSESC10b'); + PERFORM genscript_upsert_kind('ADCSESC13'); + PERFORM genscript_upsert_kind('SyncedToEntraUser'); + PERFORM genscript_upsert_kind('CoerceAndRelayNTLMToSMB'); + PERFORM genscript_upsert_kind('CoerceAndRelayNTLMToADCS'); + PERFORM genscript_upsert_kind('WriteOwnerLimitedRights'); + PERFORM genscript_upsert_kind('WriteOwnerRaw'); + PERFORM genscript_upsert_kind('OwnsLimitedRights'); + PERFORM genscript_upsert_kind('OwnsRaw'); + PERFORM genscript_upsert_kind('ClaimSpecialIdentity'); + PERFORM genscript_upsert_kind('CoerceAndRelayNTLMToLDAP'); + PERFORM genscript_upsert_kind('CoerceAndRelayNTLMToLDAPS'); + PERFORM genscript_upsert_kind('ContainsIdentity'); + PERFORM genscript_upsert_kind('PropagatesACEsTo'); + PERFORM genscript_upsert_kind('GPOAppliesTo'); + PERFORM genscript_upsert_kind('CanApplyGPO'); + PERFORM genscript_upsert_kind('HasTrustKeys'); + PERFORM genscript_upsert_kind('ProtectAdminGroups'); + + PERFORM genscript_upsert_schema_node_kind(extension_id, 'Base', 'Base', '', false, '', ''); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'User', 'User', '', true, 'fa-user', '#17E625'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'Computer', 'Computer', '', true, 'fa-desktop', '#E67873'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'Group', 'Group', '', true, 'fa-users', '#DBE617'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'GPO', 'GPO', '', true, 'fa-list', '#998EFD'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'OU', 'OU', '', true, 'fa-sitemap', '#FFAA00'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'Container', 'Container', '', true, 'fa-box', '#F79A78'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'Domain', 'Domain', '', true, 'fa-globe', '#17E6B9'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'ADLocalGroup', 'ADLocalGroup', '', false, '', ''); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'ADLocalUser', 'ADLocalUser', '', false, '', ''); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AIACA', 'AIACA', '', true, 'fa-arrows-left-right-to-line', '#9769F0'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'RootCA', 'RootCA', '', true, 'fa-landmark', '#6968E8'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'EnterpriseCA', 'EnterpriseCA', '', true, 'fa-building', '#4696E9'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'NTAuthStore', 'NTAuthStore', '', true, 'fa-store', '#D575F5'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'CertTemplate', 'CertTemplate', '', true, 'fa-id-card', '#B153F3'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'IssuancePolicy', 'IssuancePolicy', '', true, 'fa-clipboard-check', '#99B2DD'); + + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'Owns', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GenericAll', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GenericWrite', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteOwner', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteDacl', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'MemberOf', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ForceChangePassword', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AllExtendedRights', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AddMember', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'HasSession', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'Contains', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GPLink', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AllowedToDelegate', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CoerceToTGT', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GetChanges', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GetChangesAll', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GetChangesInFilteredSet', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CrossForestTrust', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'SameForestTrust', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'SpoofSIDHistory', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AbuseTGTDelegation', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AllowedToAct', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AdminTo', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CanPSRemote', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CanRDP', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ExecuteDCOM', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'HasSIDHistory', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AddSelf', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'DCSync', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ReadLAPSPassword', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ReadGMSAPassword', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'DumpSMSAPassword', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'SQLAdmin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AddAllowedToAct', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteSPN', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AddKeyCredentialLink', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'LocalToComputer', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'MemberOfLocalGroup', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'RemoteInteractiveLogonRight', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'SyncLAPSPassword', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteAccountRestrictions', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteGPLink', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'RootCAFor', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'DCFor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'PublishedTo', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ManageCertificates', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ManageCA', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'DelegatedEnrollmentAgent', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'Enroll', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'HostsCAService', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WritePKIEnrollmentFlag', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WritePKINameFlag', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'NTAuthStoreFor', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'TrustedForNTAuth', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'EnterpriseCAFor', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'IssuedSignedBy', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GoldenCert', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'EnrollOnBehalfOf', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'OIDGroupLink', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ExtendedByPolicy', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC1', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC3', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC4', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC6a', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC6b', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC9a', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC9b', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC10a', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC10b', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ADCSESC13', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'SyncedToEntraUser', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CoerceAndRelayNTLMToSMB', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CoerceAndRelayNTLMToADCS', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteOwnerLimitedRights', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'WriteOwnerRaw', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'OwnsLimitedRights', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'OwnsRaw', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ClaimSpecialIdentity', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CoerceAndRelayNTLMToLDAP', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CoerceAndRelayNTLMToLDAPS', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ContainsIdentity', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'PropagatesACEsTo', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'GPOAppliesTo', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'CanApplyGPO', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'HasTrustKeys', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'ProtectAdminGroups', '', false); END $$; + +DROP FUNCTION IF EXISTS genscript_upsert_kind; +DROP FUNCTION IF EXISTS genscript_upsert_schema_node_kind; +DROP FUNCTION IF EXISTS genscript_upsert_schema_edge_kind; diff --git a/cmd/api/src/database/migration/extensions/az_graph_schema.sql b/cmd/api/src/database/migration/extensions/az_graph_schema.sql index ace4c5a72db..24006eb7d94 100644 --- a/cmd/api/src/database/migration/extensions/az_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/az_graph_schema.sql @@ -15,156 +15,204 @@ -- SPDX-License-Identifier: Apache-2.0 -- Code generated by Cuelang code gen. DO NOT EDIT! -- Cuelang source: github.com/specterops/bloodhound/-/tree/main/packages/cue/schemas/ -INSERT INTO kind (name) VALUES - ('AZBase'), - ('AZVMScaleSet'), - ('AZApp'), - ('AZRole'), - ('AZDevice'), - ('AZFunctionApp'), - ('AZGroup'), - ('AZKeyVault'), - ('AZManagementGroup'), - ('AZResourceGroup'), - ('AZServicePrincipal'), - ('AZSubscription'), - ('AZTenant'), - ('AZUser'), - ('AZVM'), - ('AZManagedCluster'), - ('AZContainerRegistry'), - ('AZWebApp'), - ('AZLogicApp'), - ('AZAutomationAccount'), - ('AZAvereContributor'), - ('AZContains'), - ('AZContributor'), - ('AZGetCertificates'), - ('AZGetKeys'), - ('AZGetSecrets'), - ('AZHasRole'), - ('AZMemberOf'), - ('AZOwner'), - ('AZRunsAs'), - ('AZVMContributor'), - ('AZAutomationContributor'), - ('AZKeyVaultContributor'), - ('AZVMAdminLogin'), - ('AZAddMembers'), - ('AZAddSecret'), - ('AZExecuteCommand'), - ('AZGlobalAdmin'), - ('AZPrivilegedAuthAdmin'), - ('AZGrant'), - ('AZGrantSelf'), - ('AZPrivilegedRoleAdmin'), - ('AZResetPassword'), - ('AZUserAccessAdministrator'), - ('AZOwns'), - ('AZScopedTo'), - ('AZCloudAppAdmin'), - ('AZAppAdmin'), - ('AZAddOwner'), - ('AZManagedIdentity'), - ('AZMGApplication_ReadWrite_All'), - ('AZMGAppRoleAssignment_ReadWrite_All'), - ('AZMGDirectory_ReadWrite_All'), - ('AZMGGroup_ReadWrite_All'), - ('AZMGGroupMember_ReadWrite_All'), - ('AZMGRoleManagement_ReadWrite_Directory'), - ('AZMGServicePrincipalEndpoint_ReadWrite_All'), - ('AZAKSContributor'), - ('AZNodeResourceGroup'), - ('AZWebsiteContributor'), - ('AZLogicAppContributor'), - ('AZMGAddMember'), - ('AZMGAddOwner'), - ('AZMGAddSecret'), - ('AZMGGrantAppRoles'), - ('AZMGGrantRole'), - ('SyncedToADUser'), - ('AZRoleEligible'), - ('AZRoleApprover') -ON CONFLICT (name) DO NOTHING; +CREATE OR REPLACE FUNCTION genscript_upsert_kind(node_kind_name TEXT) RETURNS void AS $$ +BEGIN + IF NOT EXISTS (SELECT id FROM kind WHERE kind.name = node_kind_name) THEN + INSERT INTO kind (name) VALUES (node_kind_name); + END IF; +END $$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION genscript_upsert_schema_node_kind(v_extension_id INT, v_kind_name VARCHAR(256), v_display_name TEXT, v_description TEXT, v_is_display_kind BOOLEAN, v_icon TEXT, v_icon_color TEXT) RETURNS void AS $$ +DECLARE + upserted_kind_id SMALLINT; +BEGIN + SELECT id INTO upserted_kind_id FROM kind WHERE name = v_kind_name; + IF upserted_kind_id IS NULL THEN + RAISE EXCEPTION 'no kind name'; + END IF; + + IF NOT EXISTS (SELECT id FROM schema_node_kinds nk WHERE nk.kind_id = upserted_kind_id) THEN + INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES (v_extension_id, upserted_kind_id, v_display_name, v_description, v_is_display_kind, v_icon, v_icon_color); + ELSE + UPDATE schema_node_kinds SET display_name = v_display_name, description = v_description, is_display_kind = v_is_display_kind, icon = v_icon, icon_color = v_icon_color WHERE kind_id = upserted_kind_id; + END IF; +END; +$$ LANGUAGE plpgsql; -DELETE FROM schema_extensions WHERE name = 'AZ'; +CREATE OR REPLACE FUNCTION genscript_upsert_schema_edge_kind(v_extension_id INT, v_kind_name VARCHAR(256), v_description TEXT, v_is_traversable BOOLEAN) RETURNS void AS $$ +DECLARE + upserted_kind_id SMALLINT; +BEGIN + SELECT id INTO upserted_kind_id FROM kind WHERE name = v_kind_name; + IF upserted_kind_id IS NULL THEN + RAISE EXCEPTION 'no kind name'; + END IF; + + IF NOT EXISTS (SELECT id FROM schema_edge_kinds ek WHERE ek.kind_id = upserted_kind_id) THEN + INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES (v_extension_id, upserted_kind_id, v_description, v_is_traversable); + ELSE + UPDATE schema_edge_kinds SET description = v_description, is_traversable = v_is_traversable WHERE kind_id = upserted_kind_id; + END IF; +END; +$$ LANGUAGE plpgsql; DO $$ DECLARE - new_extension_id INT; + extension_id INT; BEGIN - INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true) RETURNING id INTO new_extension_id; + LOCK schema_extensions, schema_node_kinds, schema_edge_kinds, kind; - INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZBase'), 'Entity', '', false, '', ''), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVMScaleSet'), 'VMScaleSet', '', true, 'fa-server', '#007CD0'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZApp'), 'App', '', true, 'fa-window-restore', '#03FC84'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRole'), 'Role', '', true, 'fa-clipboard-list', '#ED8537'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZDevice'), 'Device', '', true, 'fa-desktop', '#B18FCF'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZFunctionApp'), 'FunctionApp', '', true, 'fa-bolt', '#F4BA44'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGroup'), 'Group', '', true, 'fa-users', '#F57C9B'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZKeyVault'), 'KeyVault', '', true, 'fa-lock', '#ED658C'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZManagementGroup'), 'ManagementGroup', '', true, 'fa-sitemap', '#BD93D8'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZResourceGroup'), 'ResourceGroup', '', true, 'fa-cube', '#89BD9E'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZServicePrincipal'), 'ServicePrincipal', '', true, 'fa-robot', '#C1D6D6'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZSubscription'), 'Subscription', '', true, 'fa-key', '#D2CCA1'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZTenant'), 'Tenant', '', true, 'fa-cloud', '#54F2F2'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZUser'), 'User', '', true, 'fa-user', '#34D2EB'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVM'), 'VM', '', true, 'fa-desktop', '#F9ADA0'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZManagedCluster'), 'ManagedCluster', '', true, 'fa-cubes', '#326CE5'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZContainerRegistry'), 'ContainerRegistry', '', true, 'fa-box-open', '#0885D7'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZWebApp'), 'WebApp', '', true, 'fa-object-group', '#4696E9'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZLogicApp'), 'LogicApp', '', true, 'fa-sitemap', '#9EE047'), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAutomationAccount'), 'AutomationAccount', '', true, 'fa-cog', '#F4BA44'); + IF NOT EXISTS (SELECT id FROM schema_extensions WHERE name = 'AZ') THEN + INSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('AZ', 'Azure', 'v0.0.1', true) RETURNING id INTO extension_id; + ELSE + UPDATE schema_extensions SET display_name = 'Azure', version = 'v0.0.1' WHERE name = 'AZ' RETURNING id INTO extension_id; + END IF; - INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAvereContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZContains'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGetCertificates'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGetKeys'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGetSecrets'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZHasRole'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMemberOf'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZOwner'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRunsAs'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVMContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAutomationContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZKeyVaultContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZVMAdminLogin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAddMembers'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAddSecret'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZExecuteCommand'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGlobalAdmin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZPrivilegedAuthAdmin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGrant'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZGrantSelf'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZPrivilegedRoleAdmin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZResetPassword'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZUserAccessAdministrator'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZOwns'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZScopedTo'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZCloudAppAdmin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAppAdmin'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAddOwner'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZManagedIdentity'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGApplication_ReadWrite_All'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAppRoleAssignment_ReadWrite_All'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGDirectory_ReadWrite_All'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGroup_ReadWrite_All'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGroupMember_ReadWrite_All'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGRoleManagement_ReadWrite_Directory'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGServicePrincipalEndpoint_ReadWrite_All'), '', false), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZAKSContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZNodeResourceGroup'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZWebsiteContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZLogicAppContributor'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAddMember'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAddOwner'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGAddSecret'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGrantAppRoles'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZMGGrantRole'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'SyncedToADUser'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRoleEligible'), '', true), - (new_extension_id, (SELECT id FROM kind WHERE name = 'AZRoleApprover'), '', true); + -- Insert Node Kinds + PERFORM genscript_upsert_kind('AZBase'); + PERFORM genscript_upsert_kind('AZVMScaleSet'); + PERFORM genscript_upsert_kind('AZApp'); + PERFORM genscript_upsert_kind('AZRole'); + PERFORM genscript_upsert_kind('AZDevice'); + PERFORM genscript_upsert_kind('AZFunctionApp'); + PERFORM genscript_upsert_kind('AZGroup'); + PERFORM genscript_upsert_kind('AZKeyVault'); + PERFORM genscript_upsert_kind('AZManagementGroup'); + PERFORM genscript_upsert_kind('AZResourceGroup'); + PERFORM genscript_upsert_kind('AZServicePrincipal'); + PERFORM genscript_upsert_kind('AZSubscription'); + PERFORM genscript_upsert_kind('AZTenant'); + PERFORM genscript_upsert_kind('AZUser'); + PERFORM genscript_upsert_kind('AZVM'); + PERFORM genscript_upsert_kind('AZManagedCluster'); + PERFORM genscript_upsert_kind('AZContainerRegistry'); + PERFORM genscript_upsert_kind('AZWebApp'); + PERFORM genscript_upsert_kind('AZLogicApp'); + PERFORM genscript_upsert_kind('AZAutomationAccount'); + + -- Insert Relationship Kinds + PERFORM genscript_upsert_kind('AZAvereContributor'); + PERFORM genscript_upsert_kind('AZContains'); + PERFORM genscript_upsert_kind('AZContributor'); + PERFORM genscript_upsert_kind('AZGetCertificates'); + PERFORM genscript_upsert_kind('AZGetKeys'); + PERFORM genscript_upsert_kind('AZGetSecrets'); + PERFORM genscript_upsert_kind('AZHasRole'); + PERFORM genscript_upsert_kind('AZMemberOf'); + PERFORM genscript_upsert_kind('AZOwner'); + PERFORM genscript_upsert_kind('AZRunsAs'); + PERFORM genscript_upsert_kind('AZVMContributor'); + PERFORM genscript_upsert_kind('AZAutomationContributor'); + PERFORM genscript_upsert_kind('AZKeyVaultContributor'); + PERFORM genscript_upsert_kind('AZVMAdminLogin'); + PERFORM genscript_upsert_kind('AZAddMembers'); + PERFORM genscript_upsert_kind('AZAddSecret'); + PERFORM genscript_upsert_kind('AZExecuteCommand'); + PERFORM genscript_upsert_kind('AZGlobalAdmin'); + PERFORM genscript_upsert_kind('AZPrivilegedAuthAdmin'); + PERFORM genscript_upsert_kind('AZGrant'); + PERFORM genscript_upsert_kind('AZGrantSelf'); + PERFORM genscript_upsert_kind('AZPrivilegedRoleAdmin'); + PERFORM genscript_upsert_kind('AZResetPassword'); + PERFORM genscript_upsert_kind('AZUserAccessAdministrator'); + PERFORM genscript_upsert_kind('AZOwns'); + PERFORM genscript_upsert_kind('AZScopedTo'); + PERFORM genscript_upsert_kind('AZCloudAppAdmin'); + PERFORM genscript_upsert_kind('AZAppAdmin'); + PERFORM genscript_upsert_kind('AZAddOwner'); + PERFORM genscript_upsert_kind('AZManagedIdentity'); + PERFORM genscript_upsert_kind('AZMGApplication_ReadWrite_All'); + PERFORM genscript_upsert_kind('AZMGAppRoleAssignment_ReadWrite_All'); + PERFORM genscript_upsert_kind('AZMGDirectory_ReadWrite_All'); + PERFORM genscript_upsert_kind('AZMGGroup_ReadWrite_All'); + PERFORM genscript_upsert_kind('AZMGGroupMember_ReadWrite_All'); + PERFORM genscript_upsert_kind('AZMGRoleManagement_ReadWrite_Directory'); + PERFORM genscript_upsert_kind('AZMGServicePrincipalEndpoint_ReadWrite_All'); + PERFORM genscript_upsert_kind('AZAKSContributor'); + PERFORM genscript_upsert_kind('AZNodeResourceGroup'); + PERFORM genscript_upsert_kind('AZWebsiteContributor'); + PERFORM genscript_upsert_kind('AZLogicAppContributor'); + PERFORM genscript_upsert_kind('AZMGAddMember'); + PERFORM genscript_upsert_kind('AZMGAddOwner'); + PERFORM genscript_upsert_kind('AZMGAddSecret'); + PERFORM genscript_upsert_kind('AZMGGrantAppRoles'); + PERFORM genscript_upsert_kind('AZMGGrantRole'); + PERFORM genscript_upsert_kind('SyncedToADUser'); + PERFORM genscript_upsert_kind('AZRoleEligible'); + PERFORM genscript_upsert_kind('AZRoleApprover'); + + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZBase', 'AZBase', '', false, '', ''); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZVMScaleSet', 'AZVMScaleSet', '', true, 'fa-server', '#007CD0'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZApp', 'AZApp', '', true, 'fa-window-restore', '#03FC84'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZRole', 'AZRole', '', true, 'fa-clipboard-list', '#ED8537'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZDevice', 'AZDevice', '', true, 'fa-desktop', '#B18FCF'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZFunctionApp', 'AZFunctionApp', '', true, 'fa-bolt', '#F4BA44'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZGroup', 'AZGroup', '', true, 'fa-users', '#F57C9B'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZKeyVault', 'AZKeyVault', '', true, 'fa-lock', '#ED658C'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZManagementGroup', 'AZManagementGroup', '', true, 'fa-sitemap', '#BD93D8'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZResourceGroup', 'AZResourceGroup', '', true, 'fa-cube', '#89BD9E'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZServicePrincipal', 'AZServicePrincipal', '', true, 'fa-robot', '#C1D6D6'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZSubscription', 'AZSubscription', '', true, 'fa-key', '#D2CCA1'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZTenant', 'AZTenant', '', true, 'fa-cloud', '#54F2F2'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZUser', 'AZUser', '', true, 'fa-user', '#34D2EB'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZVM', 'AZVM', '', true, 'fa-desktop', '#F9ADA0'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZManagedCluster', 'AZManagedCluster', '', true, 'fa-cubes', '#326CE5'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZContainerRegistry', 'AZContainerRegistry', '', true, 'fa-box-open', '#0885D7'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZWebApp', 'AZWebApp', '', true, 'fa-object-group', '#4696E9'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZLogicApp', 'AZLogicApp', '', true, 'fa-sitemap', '#9EE047'); + PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZAutomationAccount', 'AZAutomationAccount', '', true, 'fa-cog', '#F4BA44'); + + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAvereContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZContains', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZGetCertificates', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZGetKeys', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZGetSecrets', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZHasRole', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMemberOf', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZOwner', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZRunsAs', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZVMContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAutomationContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZKeyVaultContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZVMAdminLogin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAddMembers', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAddSecret', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZExecuteCommand', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZGlobalAdmin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZPrivilegedAuthAdmin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZGrant', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZGrantSelf', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZPrivilegedRoleAdmin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZResetPassword', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZUserAccessAdministrator', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZOwns', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZScopedTo', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZCloudAppAdmin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAppAdmin', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAddOwner', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZManagedIdentity', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGApplication_ReadWrite_All', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGAppRoleAssignment_ReadWrite_All', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGDirectory_ReadWrite_All', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGGroup_ReadWrite_All', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGGroupMember_ReadWrite_All', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGRoleManagement_ReadWrite_Directory', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGServicePrincipalEndpoint_ReadWrite_All', '', false); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZAKSContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZNodeResourceGroup', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZWebsiteContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZLogicAppContributor', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGAddMember', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGAddOwner', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGAddSecret', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGGrantAppRoles', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZMGGrantRole', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'SyncedToADUser', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZRoleEligible', '', true); + PERFORM genscript_upsert_schema_edge_kind(extension_id, 'AZRoleApprover', '', true); END $$; + +DROP FUNCTION IF EXISTS genscript_upsert_kind; +DROP FUNCTION IF EXISTS genscript_upsert_schema_node_kind; +DROP FUNCTION IF EXISTS genscript_upsert_schema_edge_kind; diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 0dfdc354a77..d7307774253 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -173,65 +173,100 @@ func GenerateExtensionSQLAzure(dir string, azSchema model.Azure) error { func GenerateExtensionSQL(name string, displayName string, version string, dir string, fileName string, nodeKinds []model.StringEnum, relationshipKinds []model.StringEnum, pathfindingRelationshipKinds []model.StringEnum) error { var sb strings.Builder - sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/\n", SchemaSourceName)) + sb.WriteString(fmt.Sprintf("-- Code generated by Cuelang code gen. DO NOT EDIT!\n-- Cuelang source: %s/", SchemaSourceName)) - sb.WriteString("INSERT INTO kind (name) VALUES\n") + sb.WriteString(` +CREATE OR REPLACE FUNCTION genscript_upsert_kind(node_kind_name TEXT) RETURNS void AS $$ +BEGIN + IF NOT EXISTS (SELECT id FROM kind WHERE kind.name = node_kind_name) THEN + INSERT INTO kind (name) VALUES (node_kind_name); + END IF; +END $$ LANGUAGE plpgsql; + `) - for _, kind := range nodeKinds { - sb.WriteString(fmt.Sprintf("\t('%s'),\n", kind.GetRepresentation())) - } + sb.WriteString(` +CREATE OR REPLACE FUNCTION genscript_upsert_schema_node_kind(v_extension_id INT, v_kind_name VARCHAR(256), v_display_name TEXT, v_description TEXT, v_is_display_kind BOOLEAN, v_icon TEXT, v_icon_color TEXT) RETURNS void AS $$ +DECLARE + upserted_kind_id SMALLINT; +BEGIN + SELECT id INTO upserted_kind_id FROM kind WHERE name = v_kind_name; + IF upserted_kind_id IS NULL THEN + RAISE EXCEPTION 'no kind name'; + END IF; + + IF NOT EXISTS (SELECT id FROM schema_node_kinds nk WHERE nk.kind_id = upserted_kind_id) THEN + INSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES (v_extension_id, upserted_kind_id, v_display_name, v_description, v_is_display_kind, v_icon, v_icon_color); + ELSE + UPDATE schema_node_kinds SET display_name = v_display_name, description = v_description, is_display_kind = v_is_display_kind, icon = v_icon, icon_color = v_icon_color WHERE kind_id = upserted_kind_id; + END IF; +END; +$$ LANGUAGE plpgsql; +`) - for i, kind := range relationshipKinds { - sb.WriteString(fmt.Sprintf("\t('%s')", kind.GetRepresentation())) - - if i != len(relationshipKinds)-1 { - sb.WriteString(",\n") - } - } + sb.WriteString(` +CREATE OR REPLACE FUNCTION genscript_upsert_schema_edge_kind(v_extension_id INT, v_kind_name VARCHAR(256), v_description TEXT, v_is_traversable BOOLEAN) RETURNS void AS $$ +DECLARE + upserted_kind_id SMALLINT; +BEGIN + SELECT id INTO upserted_kind_id FROM kind WHERE name = v_kind_name; + IF upserted_kind_id IS NULL THEN + RAISE EXCEPTION 'no kind name'; + END IF; + + IF NOT EXISTS (SELECT id FROM schema_edge_kinds ek WHERE ek.kind_id = upserted_kind_id) THEN + INSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES (v_extension_id, upserted_kind_id, v_description, v_is_traversable); + ELSE + UPDATE schema_edge_kinds SET description = v_description, is_traversable = v_is_traversable WHERE kind_id = upserted_kind_id; + END IF; +END; +$$ LANGUAGE plpgsql; +`) - sb.WriteString("\nON CONFLICT (name) DO NOTHING;\n\n") + sb.WriteString("\nDO $$\nDECLARE\n\textension_id INT;\nBEGIN\n\tLOCK schema_extensions, schema_node_kinds, schema_edge_kinds, kind;\n\n") - sb.WriteString(fmt.Sprintf("DELETE FROM schema_extensions WHERE name = '%s';\n\n", name)) + sb.WriteString(fmt.Sprintf("\tIF NOT EXISTS (SELECT id FROM schema_extensions WHERE name = '%s') THEN\n", name)) + sb.WriteString(fmt.Sprintf("\t\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('%s', '%s', '%s', true) RETURNING id INTO extension_id;\n", name, displayName, version)) + sb.WriteString("\tELSE\n") + sb.WriteString(fmt.Sprintf("\t\tUPDATE schema_extensions SET display_name = '%s', version = '%s' WHERE name = '%s' RETURNING id INTO extension_id;\n", displayName, version, name)) + sb.WriteString("\tEND IF;\n\n") - sb.WriteString("DO $$\nDECLARE\n\tnew_extension_id INT;\nBEGIN\n") + sb.WriteString("\t-- Insert Node Kinds\n") + for _, kind := range nodeKinds { + sb.WriteString(fmt.Sprintf("\tPERFORM genscript_upsert_kind('%s');\n", kind.GetRepresentation())) + } - sb.WriteString(fmt.Sprintf("\tINSERT INTO schema_extensions (name, display_name, version, is_builtin) VALUES ('%s', '%s', '%s', true) RETURNING id INTO new_extension_id;\n\n", name, displayName, version)) + sb.WriteString("\n\t-- Insert Relationship Kinds\n") + for _, kind := range relationshipKinds { + sb.WriteString(fmt.Sprintf("\tPERFORM genscript_upsert_kind('%s');\n", kind.GetRepresentation())) + } - sb.WriteString("\tINSERT INTO schema_node_kinds (schema_extension_id, kind_id, display_name, description, is_display_kind, icon, icon_color) VALUES\n") + sb.WriteString("\n") - for i, kind := range nodeKinds { - if iconInfo, found := nodeIcons[kind.GetRepresentation()]; found { - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, (SELECT id FROM kind WHERE name = '%s'), '%s', '', %t, '%s', '%s')", kind.GetRepresentation(), kind.GetName(), found, iconInfo.Icon, iconInfo.Color)) - } else { - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, (SELECT id FROM kind WHERE name = '%s'), '%s', '', %t, '', '')", kind.GetRepresentation(), kind.GetName(), found)) - } + for _, kind := range nodeKinds { + iconInfo, found := nodeIcons[kind.GetRepresentation()] - if i != len(nodeKinds)-1 { - sb.WriteString(",\n") - } + sb.WriteString(fmt.Sprintf("\tPERFORM genscript_upsert_schema_node_kind(extension_id, '%s', '%s', '', %t, '%s', '%s');\n", kind.GetRepresentation(), kind.GetRepresentation(), found, iconInfo.Icon, iconInfo.Color)) } - sb.WriteString(";\n\n") - - sb.WriteString("\tINSERT INTO schema_edge_kinds (schema_extension_id, kind_id, description, is_traversable) VALUES\n") - traversableMap := make(map[string]struct{}) for _, kind := range pathfindingRelationshipKinds { traversableMap[kind.GetRepresentation()] = struct{}{} } - for i, kind := range relationshipKinds { - _, traversable := traversableMap[kind.GetRepresentation()] + sb.WriteString("\n") - sb.WriteString(fmt.Sprintf("\t\t(new_extension_id, (SELECT id FROM kind WHERE name = '%s'), '', %t)", kind.GetRepresentation(), traversable)) + for _, kind := range relationshipKinds { + _, traversable := traversableMap[kind.GetRepresentation()] - if i != len(relationshipKinds)-1 { - sb.WriteString(",\n") - } + sb.WriteString(fmt.Sprintf("\tPERFORM genscript_upsert_schema_edge_kind(extension_id, '%s', '', %t);\n", kind.GetRepresentation(), traversable)) } - sb.WriteString(";\nEND $$;") + sb.WriteString("END $$;\n") + sb.WriteString(` +DROP FUNCTION IF EXISTS genscript_upsert_kind; +DROP FUNCTION IF EXISTS genscript_upsert_schema_node_kind; +DROP FUNCTION IF EXISTS genscript_upsert_schema_edge_kind;`) if _, err := os.Stat(dir); err != nil { if !os.IsNotExist(err) {