Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6fe1f93
feat: Add visualisation of Active Directory site data
q-roland Nov 3, 2025
892c148
feat: update AD site ingest from SharpHoundCommon
JonasBK Jun 15, 2026
7b8c31e
Merge branch 'main' into pr-2031-ad-sites
JonasBK Jun 15, 2026
6880962
fix: align AD site support with current main BED-8138
JonasBK Jun 15, 2026
52075aa
Merge branch 'main' into pr-2031-ad-sites
JonasBK Jun 15, 2026
531be7e
fix entity panels
JonasBK Jun 15, 2026
8185430
pz rule for dc site servers
JonasBK Jun 15, 2026
52aa6a1
make ServerIs traversable
JonasBK Jun 15, 2026
5fa500f
fix site nodes entity panels
JonasBK Jun 15, 2026
acc2bbd
Add cypher queries
JonasBK Jun 16, 2026
8431191
better colors
JonasBK Jun 16, 2026
9598895
add ServerIs refs in entity panel
JonasBK Jun 16, 2026
5bee575
improve edge texts
JonasBK Jun 23, 2026
067116b
fix unused parameters
JonasBK Jun 24, 2026
4ff2634
node colors
JonasBK Jun 24, 2026
ad761e1
add ServerIs to edge filter
JonasBK Jun 24, 2026
e2edb30
Merge branch 'main' into pr-2031-ad-sites
JonasBK Jun 24, 2026
7072f6c
fix schema
JonasBK Jun 24, 2026
a4630a4
fix Serverreference
JonasBK Aug 14, 2026
6ca4eaf
add siteobject and objectclass props
JonasBK Aug 14, 2026
82697a4
getGPOLinksWithoutSite
JonasBK Aug 14, 2026
691daa3
Shared container delegate
JonasBK Aug 14, 2026
9bfacb2
fix ServerIs linking
JonasBK Aug 14, 2026
010b7f0
ownership-safe migrations
JonasBK Aug 14, 2026
a38b365
direct-Site graph assertions in test
JonasBK Aug 14, 2026
e808199
Merge branch 'main' into pr-2031-ad-sites
JonasBK Aug 14, 2026
6bb8fb6
move functions
JonasBK Aug 17, 2026
9453b71
Fix ServerIs linking
JonasBK Aug 17, 2026
32e9229
Merge branch 'main' of github.com:SpecterOps/BloodHound into pr-2031-…
JonasBK Aug 17, 2026
3b4a274
improve wording of pz rules
JonasBK Aug 18, 2026
50de8fd
Update cmd/api/src/database/migration/migrations/20260615124500_v9_ad…
JonasBK Aug 18, 2026
fab1f12
Update cmd/api/src/database/migration/migrations/20260615124500_v9_ad…
JonasBK Aug 18, 2026
8b87334
Update cmd/api/src/database/migration/migrations/20260615124500_v9_ad…
JonasBK Aug 18, 2026
3951782
Update cmd/api/src/database/migration/migrations/20260615130000_v9_ad…
JonasBK Aug 18, 2026
cb699e6
fix migrations
JonasBK Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) {
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}", api.URIPathVariableObjectID), resources.GetGPOEntityInfo).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}/computers", api.URIPathVariableObjectID), resources.ListADGPOAffectedComputers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}/users", api.URIPathVariableObjectID), resources.ListADGPOAffectedUsers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}/sites", api.URIPathVariableObjectID), resources.ListADGPOAffectedSites).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}/tier-zero", api.URIPathVariableObjectID), resources.ListADGPOAffectedTierZero).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/gpos/{%s}/ous", api.URIPathVariableObjectID), resources.ListADGPOAffectedContainers).RequirePermissions(permissions.GraphDBRead),
Expand Down Expand Up @@ -347,6 +348,19 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) {
routerInst.GET(fmt.Sprintf("/api/v2/issuancepolicies/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/issuancepolicies/{%s}/linkedtemplates", api.URIPathVariableObjectID), resources.ListADIssuancePolicyLinkedCertTemplates).RequirePermissions(permissions.GraphDBRead),

// Site Entity API
routerInst.GET(fmt.Sprintf("/api/v2/sites/{%s}", api.URIPathVariableObjectID), resources.GetSiteEntityInfo).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/sites/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/sites/{%s}/linked-gpos", api.URIPathVariableObjectID), resources.ListADSiteLinkedGPOs).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/sites/{%s}/siteservers", api.URIPathVariableObjectID), resources.ListADSiteLinkedServers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/sites/{%s}/sitesubnets", api.URIPathVariableObjectID), resources.ListADSiteLinkedSubnets).RequirePermissions(permissions.GraphDBRead),

// SiteServer Entity API
routerInst.GET(fmt.Sprintf("/api/v2/siteservers/{%s}", api.URIPathVariableObjectID), resources.GetSiteServerEntityInfo).RequirePermissions(permissions.GraphDBRead),

// SiteSubnet Entity API
routerInst.GET(fmt.Sprintf("/api/v2/sitesubnets/{%s}", api.URIPathVariableObjectID), resources.GetSiteSubnetEntityInfo).RequirePermissions(permissions.GraphDBRead),

// Data Quality Stats API
routerInst.GET(fmt.Sprintf("/api/v2/ad-domains/{%s}/data-quality-stats", api.URIPathVariableDomainID), resources.GetADDataQualityStats).RequirePermissions(permissions.GraphDBRead).SupportsETAC(resources.DB, resources.DogTags),
routerInst.GET(fmt.Sprintf("/api/v2/azure-tenants/{%s}/data-quality-stats", api.URIPathVariableTenantID), resources.GetAzureDataQualityStats).RequirePermissions(permissions.GraphDBRead).SupportsETAC(resources.DB, resources.DogTags),
Expand Down
65 changes: 64 additions & 1 deletion cmd/api/src/api/v2/ad_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ type DomainPatchRequest struct {
Collected *bool `json:"collected"`
}

const (
serverReferenceComputerNameProperty = "serverreferencecomputername"
serverReferenceComputerProperty = "serverreferencecomputer"
siteServerNodeNameProperty = "siteservernodename"
siteServerNodeProperty = "siteservernode"
)

func (s *Resources) PatchDomain(response http.ResponseWriter, request *http.Request) {
var domainPatchReq DomainPatchRequest
if err := api.ReadJSONRequestPayloadLimited(&domainPatchReq, request); err != nil {
Expand Down Expand Up @@ -95,13 +102,39 @@ func (s *Resources) handleAdEntityInfoQuery(response http.ResponseWriter, reques
api.WriteBasicResponse(request.Context(), results, http.StatusOK, response)
} else {
if tiering.IsTierZero(node) {
node.Properties.Map["isTierZero"] = true
node.Properties.Set("isTierZero", true)
}

if err := s.addServerIsLinkedProperties(request, node, entityType); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("error getting linked node: %v", err), request), response)
return
}

results := map[string]any{"props": node.Properties.Map, "kinds": node.Kinds.Strings()}
api.WriteBasicResponse(request.Context(), results, http.StatusOK, response)
}
}

func (s *Resources) addServerIsLinkedProperties(request *http.Request, node *graph.Node, entityType graph.Kind) error {
if entityType.Is(ad.SiteServer) {
if linkedComputer, err := s.GraphQuery.GetEntityByRelationship(request.Context(), node, graph.DirectionOutbound, ad.ServerIs, ad.Computer); err != nil {
return err
} else if linkedComputer != nil {
node.Properties.Set(serverReferenceComputerProperty, linkedComputer.Properties.Get(common.ObjectID.String()).Any())
node.Properties.Set(serverReferenceComputerNameProperty, linkedComputer.Properties.Get(common.Name.String()).Any())
}
} else if entityType.Is(ad.Computer) {
if linkedSiteServer, err := s.GraphQuery.GetEntityByRelationship(request.Context(), node, graph.DirectionInbound, ad.ServerIs, ad.SiteServer); err != nil {
return err
} else if linkedSiteServer != nil {
node.Properties.Set(siteServerNodeProperty, linkedSiteServer.Properties.Get(common.ObjectID.String()).Any())
node.Properties.Set(siteServerNodeNameProperty, linkedSiteServer.Properties.Get(common.Name.String()).Any())
}
}

return nil
}
Comment thread
JonasBK marked this conversation as resolved.
Outdated

func (s *Resources) GetBaseEntityInfo(response http.ResponseWriter, request *http.Request) {
var (
countQueries = map[string]any{
Expand Down Expand Up @@ -176,6 +209,7 @@ func (s *Resources) GetGPOEntityInfo(response http.ResponseWriter, request *http
"ous": adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectGPOContainerCandidateFilter),
"computers": adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectComputersCandidateFilter),
"users": adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectUsersCandidateFilter),
"sites": adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectSitesCandidateFilter),
"controllers": adAnalysis.FetchInboundADEntityControllers,
"tierzero": adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectGPOTierZeroCandidateFilter),
}
Expand Down Expand Up @@ -298,3 +332,32 @@ func (s *Resources) GetIssuancePolicyEntityInfo(response http.ResponseWriter, re

s.handleAdEntityInfoQuery(response, request, ad.IssuancePolicy, countQueries)
}

func (s *Resources) GetSiteEntityInfo(response http.ResponseWriter, request *http.Request) {
var (
countQueries = map[string]any{
"controllers": adAnalysis.FetchInboundADEntityControllers,
"linkedgpos": adAnalysis.FetchEntityLinkedGPOList,
"siteServers": adAnalysis.CreateSiteContainedListDelegate(ad.SiteServer),
"siteSubnets": adAnalysis.CreateSiteContainedListDelegate(ad.SiteSubnet),
}
)

s.handleAdEntityInfoQuery(response, request, ad.Site, countQueries)
}

func (s *Resources) GetSiteServerEntityInfo(response http.ResponseWriter, request *http.Request) {
var (
countQueries = map[string]any{}
)

s.handleAdEntityInfoQuery(response, request, ad.SiteServer, countQueries)
}

func (s *Resources) GetSiteSubnetEntityInfo(response http.ResponseWriter, request *http.Request) {
var (
countQueries = map[string]any{}
)

s.handleAdEntityInfoQuery(response, request, ad.SiteSubnet, countQueries)
}
96 changes: 96 additions & 0 deletions cmd/api/src/api/v2/ad_entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/specterops/bloodhound/cmd/api/src/services/dogtags"
"github.com/specterops/bloodhound/cmd/api/src/utils/test"
"github.com/specterops/bloodhound/packages/go/graphschema/ad"
"github.com/specterops/bloodhound/packages/go/graphschema/common"
"github.com/specterops/bloodhound/packages/go/headers"
"github.com/specterops/bloodhound/packages/go/mediatypes"
"github.com/specterops/dawgs/graph"
Expand Down Expand Up @@ -144,9 +145,104 @@ func TestResources_GetComputerEntityInfo(t *testing.T) {
mockGraph.EXPECT().
GetEntityByObjectId(gomock.Any(), gomock.Any(), gomock.Any()).
Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil)
mockGraph.EXPECT().
GetEntityByRelationship(gomock.Any(), gomock.Any(), graph.DirectionInbound, ad.ServerIs, ad.SiteServer).
Return(nil, nil)
},
Test: func(output apitest.Output) {
apitest.StatusCode(output, http.StatusOK)
apitest.BodyNotContains(output, "siteservernode")
},
},
{
Name: "SuccessWithoutCountsWithSiteServer",
Input: func(input *apitest.Input) {
apitest.SetURLVar(input, "object_id", "1")
apitest.AddQueryParam(input, "counts", "false")
apitest.SetContext(input, bheCtx.ConstructGoContext())
},
Setup: func() {
siteServerProperties := graph.NewProperties().
Set(common.ObjectID.String(), "SITE-SERVER-1").
Set(common.Name.String(), "SITE-SERVER-NAME")

mockGraph.EXPECT().
GetEntityByObjectId(gomock.Any(), gomock.Any(), gomock.Any()).
Return(graph.NewNode(graph.ID(1), graph.NewProperties().Set(common.ObjectID.String(), "COMPUTER-1")), nil)
mockGraph.EXPECT().
GetEntityByRelationship(gomock.Any(), gomock.Any(), graph.DirectionInbound, ad.ServerIs, ad.SiteServer).
Return(graph.NewNode(graph.ID(2), siteServerProperties, ad.SiteServer), nil)
},
Test: func(output apitest.Output) {
apitest.StatusCode(output, http.StatusOK)
apitest.BodyContains(output, `"siteservernode":"SITE-SERVER-1"`)
apitest.BodyContains(output, `"siteservernodename":"SITE-SERVER-NAME"`)
},
},
})
}

func TestResources_GetSiteServerEntityInfo(t *testing.T) {
var (
mockCtrl = gomock.NewController(t)
mockGraph = mocks.NewMockGraph(mockCtrl)
resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})}

bheCtx = bhctx.Context{
AuthCtx: auth.Context{
PermissionOverrides: auth.PermissionOverrides{},
Owner: model.User{},
Session: model.UserSession{},
},
}
)
defer mockCtrl.Finish()

apitest.NewHarness(t, resources.GetSiteServerEntityInfo).
Run([]apitest.Case{
{
Name: "SuccessWithoutCounts",
Input: func(input *apitest.Input) {
apitest.SetURLVar(input, "object_id", "1")
apitest.AddQueryParam(input, "counts", "false")
apitest.SetContext(input, bheCtx.ConstructGoContext())
},
Setup: func() {
mockGraph.EXPECT().
GetEntityByObjectId(gomock.Any(), gomock.Any(), gomock.Any()).
Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil)
mockGraph.EXPECT().
GetEntityByRelationship(gomock.Any(), gomock.Any(), graph.DirectionOutbound, ad.ServerIs, ad.Computer).
Return(nil, nil)
},
Test: func(output apitest.Output) {
apitest.StatusCode(output, http.StatusOK)
apitest.BodyNotContains(output, "serverreferencecomputer")
},
},
{
Name: "SuccessWithoutCountsWithServerReference",
Input: func(input *apitest.Input) {
apitest.SetURLVar(input, "object_id", "1")
apitest.AddQueryParam(input, "counts", "false")
apitest.SetContext(input, bheCtx.ConstructGoContext())
},
Setup: func() {
computerProperties := graph.NewProperties().
Set(common.ObjectID.String(), "COMPUTER-1").
Set(common.Name.String(), "COMPUTER-NAME")

mockGraph.EXPECT().
GetEntityByObjectId(gomock.Any(), gomock.Any(), gomock.Any()).
Return(graph.NewNode(graph.ID(1), graph.NewProperties().Set(common.ObjectID.String(), "SITE-SERVER-1")), nil)
mockGraph.EXPECT().
GetEntityByRelationship(gomock.Any(), gomock.Any(), graph.DirectionOutbound, ad.ServerIs, ad.Computer).
Return(graph.NewNode(graph.ID(2), computerProperties, ad.Computer), nil)
},
Test: func(output apitest.Output) {
apitest.StatusCode(output, http.StatusOK)
apitest.BodyContains(output, `"serverreferencecomputer":"COMPUTER-1"`)
apitest.BodyContains(output, `"serverreferencecomputername":"COMPUTER-NAME"`)
},
},
})
Expand Down
16 changes: 16 additions & 0 deletions cmd/api/src/api/v2/ad_related_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ func (s *Resources) ListADGPOAffectedUsers(response http.ResponseWriter, request
s.handleAdRelatedEntityQuery(response, request, "ListADGPOAffectedUsers", adAnalysis.CreateGPOAffectedIntermediariesPathDelegate(ad.User), adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectUsersCandidateFilter))
}

func (s *Resources) ListADGPOAffectedSites(response http.ResponseWriter, request *http.Request) {
s.handleAdRelatedEntityQuery(response, request, "ListADGPOAffectedSites", adAnalysis.FetchGPOAffectedSitePaths, adAnalysis.FetchGPOAffectedSites)
}

func (s *Resources) ListADGPOAffectedComputers(response http.ResponseWriter, request *http.Request) {
s.handleAdRelatedEntityQuery(response, request, "ListADGPOAffectedComputers", adAnalysis.CreateGPOAffectedIntermediariesPathDelegate(ad.Computer), adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectComputersCandidateFilter))
}
Expand Down Expand Up @@ -260,3 +264,15 @@ func (s *Resources) ListTrustedCAs(response http.ResponseWriter, request *http.R
func (s *Resources) ListADCSEscalations(response http.ResponseWriter, request *http.Request) {
s.handleAdRelatedEntityQuery(response, request, "ListADCSEscalations", adAnalysis.CreateADCSEscalationsPathDelegate, adAnalysis.CreateADCSEscalationsListDelegate)
}

func (s *Resources) ListADSiteLinkedServers(response http.ResponseWriter, request *http.Request) {
s.handleAdRelatedEntityQuery(response, request, "ListADSiteLinkedServers", adAnalysis.CreateSiteContainedPathDelegate(ad.SiteServer), adAnalysis.CreateSiteContainedListDelegate(ad.SiteServer))
}

func (s *Resources) ListADSiteLinkedGPOs(response http.ResponseWriter, request *http.Request) {
s.handleAdRelatedEntityQuery(response, request, "ListADSiteLinkedGPOs", adAnalysis.FetchEntityLinkedGPOPaths, adAnalysis.FetchEntityLinkedGPOList)
}

func (s *Resources) ListADSiteLinkedSubnets(response http.ResponseWriter, request *http.Request) {
s.handleAdRelatedEntityQuery(response, request, "ListADSiteLinkedSubnets", adAnalysis.CreateSiteContainedPathDelegate(ad.SiteSubnet), adAnalysis.CreateSiteContainedListDelegate(ad.SiteSubnet))
}
8 changes: 8 additions & 0 deletions cmd/api/src/api/v2/ad_related_entity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,14 @@ func TestResources_ListADCSEscalations(t *testing.T) {
Run(setupCases(mockGraph, mockDB))
}

func TestResources_ListADSiteLinkedGPOs(t *testing.T) {
var mockCtrl, mockGraph, mockDB, resources = setup(t)
defer mockCtrl.Finish()

apitest.NewHarness(t, resources.ListADSiteLinkedGPOs).
Run(setupCases(mockGraph, mockDB))
}

func TestResources_ListADIssuancePolicyLinkedCertTemplates(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 6 additions & 0 deletions cmd/api/src/database/dataquality.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ WITH aggregated_quality_stats AS (
MAX(ntauthstores) AS max_ntauthstores,
MAX(certtemplates) AS max_certtemplates,
MAX(issuancepolicies) AS max_issuancepolicies,
MAX(sites) AS max_sites,
MAX(siteservers) AS max_siteservers,
MAX(sitesubnets) AS max_sitesubnets,
MAX(session_completeness) AS max_session_completeness,
MAX(local_group_completeness) AS max_local_group_completeness
FROM ad_data_quality_stats
Expand All @@ -121,6 +124,9 @@ SELECT
SUM(max_ntauthstores) AS ntauthstores,
SUM(max_certtemplates) AS certtemplates,
SUM(max_issuancepolicies) AS issuancepolicies,
SUM(max_sites) AS sites,
SUM(max_siteservers) AS siteservers,
SUM(max_sitesubnets) AS sitesubnets,
MAX(max_session_completeness) AS session_completeness,
MAX(max_local_group_completeness) AS local_group_completeness
FROM aggregated_quality_stats
Expand Down
15 changes: 13 additions & 2 deletions cmd/api/src/database/migration/extensions/ad_graph_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ BEGIN

RETURN kind_id;
END $$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION genscript_upsert_source_kind(kind_name TEXT) RETURNS SMALLINT AS $$
DECLARE
retrieved_kind_id SMALLINT;
Expand All @@ -45,7 +45,7 @@ BEGIN

RETURN source_kind_id;
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
retrieved_kind_id SMALLINT;
Expand Down Expand Up @@ -171,6 +171,9 @@ BEGIN
PERFORM genscript_upsert_kind('NTAuthStore');
PERFORM genscript_upsert_kind('CertTemplate');
PERFORM genscript_upsert_kind('IssuancePolicy');
PERFORM genscript_upsert_kind('Site');
PERFORM genscript_upsert_kind('SiteServer');
PERFORM genscript_upsert_kind('SiteSubnet');

-- Insert Relationship Kinds
PERFORM genscript_upsert_kind('Owns');
Expand All @@ -184,6 +187,7 @@ BEGIN
PERFORM genscript_upsert_kind('AddMember');
PERFORM genscript_upsert_kind('HasSession');
PERFORM genscript_upsert_kind('Contains');
PERFORM genscript_upsert_kind('ServerIs');
PERFORM genscript_upsert_kind('GPLink');
PERFORM genscript_upsert_kind('AllowedToDelegate');
PERFORM genscript_upsert_kind('CoerceToTGT');
Expand Down Expand Up @@ -278,6 +282,9 @@ BEGIN
PERFORM genscript_upsert_schema_node_kind(extension_id, 'NTAuthStore', 'NTAuthStore', '', true, 'store', '#D575F5');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'CertTemplate', 'CertTemplate', '', true, 'id-card', '#B153F3');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'IssuancePolicy', 'IssuancePolicy', '', true, 'clipboard-check', '#99B2DD');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'Site', 'Site', '', true, 'map-signs', '#2DD4BF');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'SiteServer', 'SiteServer', '', true, 'map-marker', '#60A5FA');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'SiteSubnet', 'SiteSubnet', '', true, 'map', '#F59E0B');

-- Keep custom_node_kinds in sync with node kinds
PERFORM genscript_upsert_custom_node_kind('User', '{"icon": {"name": "user", "type": "font-awesome", "color": "#17E625"}}');
Expand All @@ -293,6 +300,9 @@ BEGIN
PERFORM genscript_upsert_custom_node_kind('NTAuthStore', '{"icon": {"name": "store", "type": "font-awesome", "color": "#D575F5"}}');
PERFORM genscript_upsert_custom_node_kind('CertTemplate', '{"icon": {"name": "id-card", "type": "font-awesome", "color": "#B153F3"}}');
PERFORM genscript_upsert_custom_node_kind('IssuancePolicy', '{"icon": {"name": "clipboard-check", "type": "font-awesome", "color": "#99B2DD"}}');
PERFORM genscript_upsert_custom_node_kind('Site', '{"icon": {"name": "map-signs", "type": "font-awesome", "color": "#2DD4BF"}}');
PERFORM genscript_upsert_custom_node_kind('SiteServer', '{"icon": {"name": "map-marker", "type": "font-awesome", "color": "#60A5FA"}}');
PERFORM genscript_upsert_custom_node_kind('SiteSubnet', '{"icon": {"name": "map", "type": "font-awesome", "color": "#F59E0B"}}');

PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'Owns', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'GenericAll', '', true);
Expand All @@ -305,6 +315,7 @@ BEGIN
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AddMember', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'HasSession', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'Contains', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'ServerIs', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'GPLink', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AllowedToDelegate', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'CoerceToTGT', '', true);
Expand Down
Loading
Loading