diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index 5202367ba75f..5791d5c3d52a 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -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), @@ -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), diff --git a/cmd/api/src/api/v2/ad_entity.go b/cmd/api/src/api/v2/ad_entity.go index e77dab890e04..36b4c6802161 100644 --- a/cmd/api/src/api/v2/ad_entity.go +++ b/cmd/api/src/api/v2/ad_entity.go @@ -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 { @@ -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 +} + func (s *Resources) GetBaseEntityInfo(response http.ResponseWriter, request *http.Request) { var ( countQueries = map[string]any{ @@ -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), } @@ -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) +} diff --git a/cmd/api/src/api/v2/ad_entity_test.go b/cmd/api/src/api/v2/ad_entity_test.go index 69ccf59f8f45..aeebd323ce34 100644 --- a/cmd/api/src/api/v2/ad_entity_test.go +++ b/cmd/api/src/api/v2/ad_entity_test.go @@ -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" @@ -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"`) }, }, }) diff --git a/cmd/api/src/api/v2/ad_related_entity.go b/cmd/api/src/api/v2/ad_related_entity.go index 25188661a6c8..40f5090bcaee 100644 --- a/cmd/api/src/api/v2/ad_related_entity.go +++ b/cmd/api/src/api/v2/ad_related_entity.go @@ -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)) } @@ -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)) +} diff --git a/cmd/api/src/api/v2/ad_related_entity_test.go b/cmd/api/src/api/v2/ad_related_entity_test.go index c28039ff903e..3fdc6b0f7c27 100644 --- a/cmd/api/src/api/v2/ad_related_entity_test.go +++ b/cmd/api/src/api/v2/ad_related_entity_test.go @@ -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() diff --git a/cmd/api/src/database/dataquality.go b/cmd/api/src/database/dataquality.go index 9b1642df5702..2d34509bd8cc 100644 --- a/cmd/api/src/database/dataquality.go +++ b/cmd/api/src/database/dataquality.go @@ -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 @@ -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 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 813d99cda969..5f05db4fc09a 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -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; @@ -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; @@ -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'); @@ -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'); @@ -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"}}'); @@ -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); @@ -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); 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 fb7af3e6f889..ecb5d21df2d2 100644 --- a/cmd/api/src/database/migration/extensions/az_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/az_graph_schema.sql @@ -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; @@ -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; diff --git a/cmd/api/src/database/migration/migrations/20260615124500_v9_add_ad_site_data_quality.sql b/cmd/api/src/database/migration/migrations/20260615124500_v9_add_ad_site_data_quality.sql new file mode 100644 index 000000000000..fd1757e3501a --- /dev/null +++ b/cmd/api/src/database/migration/migrations/20260615124500_v9_add_ad_site_data_quality.sql @@ -0,0 +1,99 @@ +-- Copyright 2026 Specter Ops, Inc. +-- +-- Licensed under the Apache License, Version 2.0 +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- +goose Up +ALTER TABLE ad_data_quality_aggregations ADD COLUMN IF NOT EXISTS sites BIGINT DEFAULT 0; +ALTER TABLE ad_data_quality_aggregations ADD COLUMN IF NOT EXISTS siteservers BIGINT DEFAULT 0; +ALTER TABLE ad_data_quality_aggregations ADD COLUMN IF NOT EXISTS sitesubnets BIGINT DEFAULT 0; + +ALTER TABLE ad_data_quality_stats ADD COLUMN IF NOT EXISTS sites BIGINT DEFAULT 0; +ALTER TABLE ad_data_quality_stats ADD COLUMN IF NOT EXISTS siteservers BIGINT DEFAULT 0; +ALTER TABLE ad_data_quality_stats ADD COLUMN IF NOT EXISTS sitesubnets BIGINT DEFAULT 0; + +WITH src_data AS ( + SELECT * + FROM (VALUES + ( + 'Sites', + true, + true, + E'MATCH (n:Site) \nRETURN n;', + E'Control over an Active Directory site may allow users to compromise all assets associated with the site through the application of Group Policy Objects. Since AD Sites contain at least a Domain Controller as a Site Server, this results in the potential compromise of at least one domain in the forest. Therefore, Active Directory Site objects are Tier Zero.' + ) + ) AS s (name, enabled, allow_disable, cypher, description) +), +inserted_selectors AS ( + INSERT INTO asset_group_tag_selectors ( + asset_group_tag_id, + created_at, + created_by, + updated_at, + updated_by, + disabled_at, + disabled_by, + name, + description, + is_default, + allow_disable, + auto_certify + ) + SELECT + (SELECT id FROM asset_group_tags WHERE type = 1 AND position = 1 LIMIT 1), + current_timestamp, + 'BloodHound', + current_timestamp, + 'BloodHound', + CASE WHEN NOT d.enabled THEN current_timestamp ELSE NULL END, + CASE WHEN NOT d.enabled THEN 'BloodHound' ELSE NULL END, + d.name, + d.description, + true, + d.allow_disable, + 2 + FROM src_data d + WHERE NOT EXISTS (SELECT 1 FROM asset_group_tag_selectors WHERE name = d.name) + RETURNING id, name +) +INSERT INTO asset_group_tag_selector_seeds (selector_id, type, value) +SELECT + s.id, + 2, + d.cypher +FROM inserted_selectors s +JOIN src_data d ON d.name = s.name; + +-- +goose Down +DELETE FROM asset_group_tag_selector_seeds +WHERE selector_id IN ( + SELECT id + FROM asset_group_tag_selectors + WHERE name = 'Sites' + AND is_default = true + AND created_by = 'BloodHound' +); + +DELETE FROM asset_group_tag_selectors +WHERE name = 'Sites' + AND is_default = true + AND created_by = 'BloodHound'; + +ALTER TABLE ad_data_quality_stats DROP COLUMN IF EXISTS sitesubnets; +ALTER TABLE ad_data_quality_stats DROP COLUMN IF EXISTS siteservers; +ALTER TABLE ad_data_quality_stats DROP COLUMN IF EXISTS sites; + +ALTER TABLE ad_data_quality_aggregations DROP COLUMN IF EXISTS sitesubnets; +ALTER TABLE ad_data_quality_aggregations DROP COLUMN IF EXISTS siteservers; +ALTER TABLE ad_data_quality_aggregations DROP COLUMN IF EXISTS sites; diff --git a/cmd/api/src/database/migration/migrations/20260615130000_v9_add_tier_zero_site_server_selector.sql b/cmd/api/src/database/migration/migrations/20260615130000_v9_add_tier_zero_site_server_selector.sql new file mode 100644 index 000000000000..40d9d6d0f632 --- /dev/null +++ b/cmd/api/src/database/migration/migrations/20260615130000_v9_add_tier_zero_site_server_selector.sql @@ -0,0 +1,83 @@ +-- Copyright 2026 Specter Ops, Inc. +-- +-- Licensed under the Apache License, Version 2.0 +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- +goose Up +WITH src_data AS ( + SELECT * + FROM (VALUES + ( + 'Domain Controller Site Servers', + true, + false, + E'MATCH (n:SiteServer)-[:ServerIs]->(:Computer)-[:DCFor]->(:Domain)\nRETURN n;', + E'Active Directory Site Server objects that reference domain controllers are Tier Zero because control over the Site Server object may impact a domain controller and therefore the domain.' + ) + ) AS s (name, enabled, allow_disable, cypher, description) +), +inserted_selectors AS ( + INSERT INTO asset_group_tag_selectors ( + asset_group_tag_id, + created_at, + created_by, + updated_at, + updated_by, + disabled_at, + disabled_by, + name, + description, + is_default, + allow_disable, + auto_certify + ) + SELECT + (SELECT id FROM asset_group_tags WHERE type = 1 AND position = 1 LIMIT 1), + current_timestamp, + 'BloodHound', + current_timestamp, + 'BloodHound', + CASE WHEN NOT d.enabled THEN current_timestamp ELSE NULL END, + CASE WHEN NOT d.enabled THEN 'BloodHound' ELSE NULL END, + d.name, + d.description, + true, + d.allow_disable, + 2 + FROM src_data d + WHERE NOT EXISTS (SELECT 1 FROM asset_group_tag_selectors WHERE name = d.name) + RETURNING id, name +) +INSERT INTO asset_group_tag_selector_seeds (selector_id, type, value) +SELECT + s.id, + 2, + d.cypher +FROM inserted_selectors s +JOIN src_data d ON d.name = s.name; + +-- +goose Down +DELETE FROM asset_group_tag_selector_seeds +WHERE selector_id IN ( + SELECT id + FROM asset_group_tag_selectors + WHERE name = 'Domain Controller Site Servers' + AND is_default = true + AND created_by = 'BloodHound' +); + +DELETE FROM asset_group_tag_selectors +WHERE name = 'Domain Controller Site Servers' + AND is_default = true + AND created_by = 'BloodHound'; diff --git a/cmd/api/src/model/adquality.go b/cmd/api/src/model/adquality.go index 4ee6ea9cc493..2380a5244d65 100644 --- a/cmd/api/src/model/adquality.go +++ b/cmd/api/src/model/adquality.go @@ -34,6 +34,9 @@ type ADDataQualityStat struct { NTAuthStores int `json:"ntauthstores" gorm:"column:ntauthstores"` CertTemplates int `json:"certtemplates" gorm:"column:certtemplates"` IssuancePolicies int `json:"issuancepolicies" gorm:"column:issuancepolicies"` + Sites int `json:"sites" gorm:"column:sites"` + SiteServers int `json:"siteservers" gorm:"column:siteservers"` + SiteSubnets int `json:"sitesubnets" gorm:"column:sitesubnets"` ACLs int `json:"acls" gorm:"column:acls"` Sessions int `json:"sessions"` Relationships int `json:"relationships"` @@ -58,6 +61,9 @@ type ADDataQualityAggregation struct { NTAuthStores int `json:"ntauthstores" gorm:"column:ntauthstores"` CertTemplates int `json:"certtemplates" gorm:"column:certtemplates"` IssuancePolicies int `json:"issuancepolicies" gorm:"column:issuancepolicies"` + Sites int `json:"sites" gorm:"column:sites"` + SiteServers int `json:"siteservers" gorm:"column:siteservers"` + SiteSubnets int `json:"sitesubnets" gorm:"column:sitesubnets"` Acls int `json:"acls" gorm:"column:acls"` Sessions int `json:"sessions"` Relationships int `json:"relationships"` diff --git a/cmd/api/src/model/ingest/ingest.go b/cmd/api/src/model/ingest/ingest.go index 9e660a895eb7..6a05abfa0dce 100644 --- a/cmd/api/src/model/ingest/ingest.go +++ b/cmd/api/src/model/ingest/ingest.go @@ -61,6 +61,9 @@ const ( DataTypeCertTemplate DataType = "certtemplates" DataTypeAzure DataType = "azure" DataTypeIssuancePolicy DataType = "issuancepolicies" + DataTypeSite DataType = "sites" + DataTypeSiteServer DataType = "siteservers" + DataTypeSiteSubnet DataType = "sitesubnets" DataTypeOpenGraph DataType = "opengraph" ) @@ -83,6 +86,9 @@ func AllOriginalIngestDataTypes() []DataType { DataTypeCertTemplate, DataTypeAzure, DataTypeIssuancePolicy, + DataTypeSite, + DataTypeSiteServer, + DataTypeSiteSubnet, } } diff --git a/cmd/api/src/queries/graph.go b/cmd/api/src/queries/graph.go index d676ecc4b567..7b83469046f0 100644 --- a/cmd/api/src/queries/graph.go +++ b/cmd/api/src/queries/graph.go @@ -144,6 +144,7 @@ type Graph interface { SearchByNameOrObjectID(ctx context.Context, includeOpenGraphNodes bool, searchValue string, searchType string) (graph.NodeSet, error) GetADEntityQueryResult(ctx context.Context, primaryNodeKinds graphschema.PrimaryDisplayKinds, params EntityQueryParameters, cacheEnabled bool) (any, int, error) GetEntityByObjectId(ctx context.Context, objectID string, kinds ...graph.Kind) (*graph.Node, error) + GetEntityByRelationship(ctx context.Context, node *graph.Node, direction graph.Direction, relationshipKind graph.Kind, relatedKind graph.Kind) (*graph.Node, error) GetEntityCountResults(ctx context.Context, node *graph.Node, delegates map[string]any) map[string]any GetNodesByKind(ctx context.Context, kinds ...graph.Kind) (graph.NodeSet, error) GetPrimaryNodeKindCounts(ctx context.Context, primaryDisplayKinds graphschema.PrimaryDisplayKinds, kind graph.Kind, additionalFilters ...graph.Criteria) (map[string]int, error) @@ -646,6 +647,47 @@ func (s *GraphQuery) GetEntityByObjectId(ctx context.Context, objectID string, k } } +func (s *GraphQuery) GetEntityByRelationship(ctx context.Context, node *graph.Node, direction graph.Direction, relationshipKind graph.Kind, relatedKind graph.Kind) (*graph.Node, error) { + var ( + err error + relatedNode *graph.Node + relatedSet graph.NodeSet + ) + + if err = s.Graph.ReadTransaction(ctx, func(tx graph.Transaction) error { + relationshipQuery := tx.Relationships().Filterf(func() graph.Criteria { + if direction == graph.DirectionInbound { + return query.And( + query.Kind(query.Start(), relatedKind), + query.Kind(query.Relationship(), relationshipKind), + query.Equals(query.EndID(), node.ID), + ) + } + + return query.And( + query.Equals(query.StartID(), node.ID), + query.Kind(query.Relationship(), relationshipKind), + query.Kind(query.End(), relatedKind), + ) + }) + + if direction == graph.DirectionInbound { + relatedSet, err = ops.FetchStartNodes(relationshipQuery) + } else { + relatedSet, err = ops.FetchEndNodes(relationshipQuery) + } + + return err + }); err != nil { + return nil, err + } else if relatedSet.Len() == 0 { + return nil, nil + } else { + relatedNode = relatedSet.Pick() + return relatedNode, nil + } +} + func (s *GraphQuery) GetEntityCountResults(ctx context.Context, node *graph.Node, delegates map[string]any) map[string]any { var ( results = make(map[string]any) diff --git a/cmd/api/src/queries/mocks/graph.go b/cmd/api/src/queries/mocks/graph.go index b604c989dbd5..441d1683f30a 100644 --- a/cmd/api/src/queries/mocks/graph.go +++ b/cmd/api/src/queries/mocks/graph.go @@ -262,6 +262,21 @@ func (mr *MockGraphMockRecorder) GetEntityByObjectId(ctx, objectID any, kinds .. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntityByObjectId", reflect.TypeOf((*MockGraph)(nil).GetEntityByObjectId), varargs...) } +// GetEntityByRelationship mocks base method. +func (m *MockGraph) GetEntityByRelationship(ctx context.Context, node *graph.Node, direction graph.Direction, relationshipKind, relatedKind graph.Kind) (*graph.Node, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEntityByRelationship", ctx, node, direction, relationshipKind, relatedKind) + ret0, _ := ret[0].(*graph.Node) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEntityByRelationship indicates an expected call of GetEntityByRelationship. +func (mr *MockGraphMockRecorder) GetEntityByRelationship(ctx, node, direction, relationshipKind, relatedKind any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntityByRelationship", reflect.TypeOf((*MockGraph)(nil).GetEntityByRelationship), ctx, node, direction, relationshipKind, relatedKind) +} + // GetEntityCountResults mocks base method. func (m *MockGraph) GetEntityCountResults(ctx context.Context, node *graph.Node, delegates map[string]any) map[string]any { m.ctrl.T.Helper() diff --git a/cmd/api/src/services/dataquality/dataquality.go b/cmd/api/src/services/dataquality/dataquality.go index 981bd50ba295..37a9becf8f9b 100644 --- a/cmd/api/src/services/dataquality/dataquality.go +++ b/cmd/api/src/services/dataquality/dataquality.go @@ -150,6 +150,18 @@ func adGraphStats(ctx context.Context, db graph.Database) (model.ADDataQualitySt stat.IssuancePolicies = int(count) aggregation.IssuancePolicies += int(count) + case ad.Site: + stat.Sites = int(count) + aggregation.Sites += int(count) + + case ad.SiteServer: + stat.SiteServers = int(count) + aggregation.SiteServers += int(count) + + case ad.SiteSubnet: + stat.SiteSubnets = int(count) + aggregation.SiteSubnets += int(count) + case ad.Domain: // Do nothing. Only ADDataQualityAggregation stats have domain stats and the domain stats are handled in the outer domain loop } diff --git a/cmd/api/src/services/graphify/convertors.go b/cmd/api/src/services/graphify/convertors.go index 267bda0aa5dc..46763b38433b 100644 --- a/cmd/api/src/services/graphify/convertors.go +++ b/cmd/api/src/services/graphify/convertors.go @@ -326,3 +326,40 @@ func convertIssuancePolicy(issuancePolicy ein.IssuancePolicy, converted *Convert converted.RelProps = append(converted.RelProps, container) } } + +func convertSiteData(site ein.Site, converted *ConvertedData, ingestTime time.Time) { + baseNodeProp := ein.ConvertSiteToNode(site, ingestTime) + converted.NodeProps = append(converted.NodeProps, baseNodeProp) + converted.RelProps = append(converted.RelProps, ein.ParseACEData(baseNodeProp, site.Aces, site.ObjectIdentifier, ad.Site)...) + + if rel := ein.ParseObjectContainer(site.IngestBase, ad.Site); rel.IsValid() { + converted.RelProps = append(converted.RelProps, rel) + } + + if len(site.ChildObjects) > 0 { + converted.RelProps = append(converted.RelProps, ein.ParseChildObjects(site.ChildObjects, site.ObjectIdentifier, ad.Site)...) + } + + if len(site.Links) > 0 { + converted.RelProps = append(converted.RelProps, ein.ParseGpLinks(site.Links, site.ObjectIdentifier, ad.Site)...) + } +} + +func convertSiteServerData(siteServer ein.SiteServer, converted *ConvertedData, ingestTime time.Time) { + baseNodeProp := ein.ConvertObjectToNode(siteServer.IngestBase, ad.SiteServer, ingestTime) + converted.NodeProps = append(converted.NodeProps, baseNodeProp) + converted.RelProps = append(converted.RelProps, ein.ParseSiteServerData(siteServer)...) + + if rel := ein.ParseObjectContainer(siteServer.IngestBase, ad.SiteServer); rel.IsValid() { + converted.RelProps = append(converted.RelProps, rel) + } +} + +func convertSiteSubnetData(siteSubnet ein.SiteSubnet, converted *ConvertedData, ingestTime time.Time) { + baseNodeProp := ein.ConvertObjectToNode(ein.IngestBase(siteSubnet), ad.SiteSubnet, ingestTime) + converted.NodeProps = append(converted.NodeProps, baseNodeProp) + + if rel := ein.ParseObjectContainer(ein.IngestBase(siteSubnet), ad.SiteSubnet); rel.IsValid() { + converted.RelProps = append(converted.RelProps, rel) + } +} diff --git a/cmd/api/src/services/graphify/ingest.go b/cmd/api/src/services/graphify/ingest.go index 9a41b907b11f..5f0adba9d048 100644 --- a/cmd/api/src/services/graphify/ingest.go +++ b/cmd/api/src/services/graphify/ingest.go @@ -399,6 +399,9 @@ var basicHandlers = map[ingest.DataType]basicIngestHandler{ ingest.DataTypeNTAuthStore: defaultBasicHandler(convertNTAuthStoreData), ingest.DataTypeCertTemplate: defaultBasicHandler(convertCertTemplateData), ingest.DataTypeIssuancePolicy: defaultBasicHandler(convertIssuancePolicy), + ingest.DataTypeSite: defaultBasicHandler(convertSiteData), + ingest.DataTypeSiteServer: defaultBasicHandler(convertSiteServerData), + ingest.DataTypeSiteSubnet: defaultBasicHandler(convertSiteSubnetData), } var sourceKindHandlers = map[ingest.DataType]sourceKindIngestHandler{ diff --git a/cmd/ui/src/ducks/graph/graphutils.ts b/cmd/ui/src/ducks/graph/graphutils.ts index b444568363f9..898a1f2a4445 100644 --- a/cmd/ui/src/ducks/graph/graphutils.ts +++ b/cmd/ui/src/ducks/graph/graphutils.ts @@ -236,6 +236,9 @@ const ICONS: { [id in GraphNodeTypes]: string } = { [GraphNodeTypes.NTAuthStore]: 'fa-store', [GraphNodeTypes.CertTemplate]: 'fa-id-card', [GraphNodeTypes.IssuancePolicy]: 'fa-clipboard-check', + [GraphNodeTypes.Site]: 'fa-map-signs', + [GraphNodeTypes.SiteServer]: 'fa-map-marker', + [GraphNodeTypes.SiteSubnet]: 'fa-map', }; const setFontIcons = (data: Items): void => { diff --git a/cmd/ui/src/ducks/graph/types.ts b/cmd/ui/src/ducks/graph/types.ts index 3ee5002d3ba2..1248f1d225a1 100644 --- a/cmd/ui/src/ducks/graph/types.ts +++ b/cmd/ui/src/ducks/graph/types.ts @@ -49,6 +49,9 @@ export enum GraphNodeTypes { NTAuthStore = 'NTAuthStore', CertTemplate = 'CertTemplate', IssuancePolicy = 'IssuancePolicy', + Site = 'Site', + SiteServer = 'SiteServer', + SiteSubnet = 'SiteSubnet', } export interface GraphNodeData { diff --git a/packages/csharp/graphschema/PropertyNames.cs b/packages/csharp/graphschema/PropertyNames.cs index 66c35f6af8fc..340c2c43ec3d 100644 --- a/packages/csharp/graphschema/PropertyNames.cs +++ b/packages/csharp/graphschema/PropertyNames.cs @@ -179,6 +179,7 @@ public static class PropertyNames { public static readonly string NetBIOS = "netbios"; public static readonly string AdminSDHolderProtected = "adminsdholderprotected"; public static readonly string ServicePrincipalNames = "serviceprincipalnames"; +public static readonly string Serverreference = "serverreference"; public static readonly string GPOStatusRaw = "gpostatusraw"; public static readonly string GPOStatus = "gpostatus"; diff --git a/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index f7ff5f73a852..9831812442b7 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -1023,6 +1023,13 @@ ServicePrincipalNames: types.#StringEnum & { representation: "serviceprincipalnames" } +Serverreference: types.#StringEnum & { + symbol: "Serverreference" + schema: "ad" + name: "Server Reference" + representation: "serverreference" +} + GPOStatusRaw: types.#StringEnum & { symbol: "GPOStatusRaw" schema: "ad" @@ -1176,6 +1183,7 @@ Properties: [ NetBIOS, AdminSDHolderProtected, ServicePrincipalNames, + Serverreference, GPOStatusRaw, GPOStatus, ] @@ -1264,6 +1272,21 @@ IssuancePolicy: types.#Kind & { schema: "active_directory" } +Site: types.#Kind & { + symbol: "Site" + schema: "active_directory" +} + +SiteServer: types.#Kind & { + symbol: "SiteServer" + schema: "active_directory" +} + +SiteSubnet: types.#Kind & { + symbol: "SiteSubnet" + schema: "active_directory" +} + NodeKinds: [ Entity, User, @@ -1281,6 +1304,9 @@ NodeKinds: [ NTAuthStore, CertTemplate, IssuancePolicy, + Site, + SiteServer, + SiteSubnet ] Owns: types.#Kind & { @@ -1349,6 +1375,11 @@ Contains: types.#Kind & { schema: "active_directory" } +ServerIs: types.#Kind & { + symbol: "ServerIs" + schema: "active_directory" +} + GPLink: types.#Kind & { symbol: "GPLink" schema: "active_directory" @@ -1743,6 +1774,7 @@ RelationshipKinds: [ AddMember, HasSession, Contains, + ServerIs, GPLink, AllowedToDelegate, CoerceToTGT, @@ -1919,6 +1951,7 @@ SharedRelationshipKinds: [ WritePublicInformation, ManageCA, ManageCertificates, + ServerIs, ] // Edges that are used during inbound traversal diff --git a/packages/go/analysis/ad/ad_integration_test.go b/packages/go/analysis/ad/ad_integration_test.go index 3784f45cf7c0..74bd77bf21b4 100644 --- a/packages/go/analysis/ad/ad_integration_test.go +++ b/packages/go/analysis/ad/ad_integration_test.go @@ -752,6 +752,50 @@ func TestFetchGPOAffectedContainerPaths(t *testing.T) { }) } +func TestFetchGPOAffectedSites(t *testing.T) { + var ( + testContext = integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + containedSite *graph.Node + directSite *graph.Node + ) + + testContext.WriteTransactionTestWithSetup(func(harness *integration.HarnessDetails) error { + var ( + domainSID string + err error + ) + + harness.GPOEnforcement.Setup(testContext) + + if domainSID, err = harness.GPOEnforcement.Domain.Properties.Get(ad.DomainSID.String()).String(); err != nil { + return err + } + + directSite = testContext.NewNode(graph.AsProperties(graph.PropertyMap{ + common.Name: "Direct Site", + common.ObjectID: "direct-site", + ad.DomainSID: domainSID, + }), ad.Entity, ad.Site) + + containedSite = testContext.NewNode(graph.AsProperties(graph.PropertyMap{ + common.Name: "Contained Site", + common.ObjectID: "contained-site", + ad.DomainSID: domainSID, + }), ad.Entity, ad.Site) + + testContext.NewRelationship(harness.GPOEnforcement.GPOEnforced, directSite, ad.GPLink, integration.DefaultRelProperties) + testContext.NewRelationship(harness.GPOEnforcement.Domain, containedSite, ad.Contains, integration.DefaultRelProperties) + return nil + }, func(harness integration.HarnessDetails, tx graph.Transaction) { + affectedSites, err := adAnalysis.FetchGPOAffectedSites(tx, harness.GPOEnforcement.GPOEnforced, 0, 0) + + test.RequireNilErr(t, err) + require.Equal(t, 1, affectedSites.Len()) + require.Contains(t, affectedSites.IDs(), directSite.ID) + require.NotContains(t, affectedSites.IDs(), containedSite.ID) + }) +} + func TestCreateGPOAffectedIntermediariesListDelegateAffectedContainers(t *testing.T) { testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) testContext.WriteTransactionTestWithSetup(func(harness *integration.HarnessDetails) error { @@ -1659,16 +1703,55 @@ func TestFetchAllEnforcedGPOs(t *testing.T) { } func TestFetchEntityLinkedGPOList(t *testing.T) { - testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + var ( + testContext = integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + containedSite *graph.Node + directSite *graph.Node + ) testContext.ReadTransactionTestWithSetup(func(harness *integration.HarnessDetails) error { + var ( + domainSID string + err error + ) + harness.GPOEnforcement.Setup(testContext) + + if domainSID, err = harness.GPOEnforcement.Domain.Properties.Get(ad.DomainSID.String()).String(); err != nil { + return err + } + + directSite = testContext.NewNode(graph.AsProperties(graph.PropertyMap{ + common.Name: "Direct Linked Site", + common.ObjectID: "direct-linked-site", + ad.DomainSID: domainSID, + }), ad.Entity, ad.Site) + + containedSite = testContext.NewNode(graph.AsProperties(graph.PropertyMap{ + common.Name: "Contained Linked Site", + common.ObjectID: "contained-linked-site", + ad.DomainSID: domainSID, + }), ad.Entity, ad.Site) + + testContext.NewRelationship(harness.GPOEnforcement.GPOEnforced, directSite, ad.GPLink, integration.DefaultRelProperties) + testContext.NewRelationship(harness.GPOEnforcement.Domain, containedSite, ad.Contains, integration.DefaultRelProperties) return nil }, func(harness integration.HarnessDetails, tx graph.Transaction) { gpos, err := adAnalysis.FetchEntityLinkedGPOList(tx, harness.GPOEnforcement.Domain, 0, 0) test.RequireNilErr(t, err) require.Equal(t, 2, gpos.Len()) + + gpos, err = adAnalysis.FetchEntityLinkedGPOList(tx, directSite, 0, 0) + + test.RequireNilErr(t, err) + require.Equal(t, 1, gpos.Len()) + require.Contains(t, gpos.IDs(), harness.GPOEnforcement.GPOEnforced.ID) + + gpos, err = adAnalysis.FetchEntityLinkedGPOList(tx, containedSite, 0, 0) + + test.RequireNilErr(t, err) + require.Equal(t, 0, gpos.Len()) }) } diff --git a/packages/go/analysis/ad/filters.go b/packages/go/analysis/ad/filters.go index c58d0c929f8b..5ccacfa8c31b 100644 --- a/packages/go/analysis/ad/filters.go +++ b/packages/go/analysis/ad/filters.go @@ -154,6 +154,10 @@ func SelectUsersCandidateFilter(node *graph.Node) bool { return node.Kinds.ContainsOneOf(ad.User) } +func SelectSitesCandidateFilter(node *graph.Node) bool { + return node.Kinds.ContainsOneOf(ad.Site) +} + func SelectComputersCandidateFilter(node *graph.Node) bool { return node.Kinds.ContainsOneOf(ad.Computer) } diff --git a/packages/go/analysis/ad/queries.go b/packages/go/analysis/ad/queries.go index e1fdc0c0c103..acff0c2d447f 100644 --- a/packages/go/analysis/ad/queries.go +++ b/packages/go/analysis/ad/queries.go @@ -212,7 +212,7 @@ func getGPOLinks(tx graph.Transaction, node *graph.Node) ([]*graph.Relationship, return query.And( query.Equals(query.StartID(), node.ID), query.Kind(query.Relationship(), ad.GPLink), - query.KindIn(query.End(), ad.Domain, ad.OU), + query.KindIn(query.End(), ad.Domain, ad.OU, ad.Site), ) })); err != nil { return nil, err @@ -336,6 +336,10 @@ func FetchGPOAffectedContainerPaths(tx graph.Transaction, node *graph.Node) (gra if _, end, err := ops.FetchRelationshipNodes(tx, rel); err != nil { return nil, err } else { + if end.Kinds.ContainsOneOf(ad.Site) { + // We don't want to handle Sites here, only domain and OUs + continue + } var descentFilter ops.SegmentFilter // Set our descent filter based on enforcement status @@ -375,6 +379,57 @@ func FetchGPOAffectedContainerPaths(tx graph.Transaction, node *graph.Node) (gra } } +func FetchGPOAffectedSitePaths(tx graph.Transaction, node *graph.Node) (graph.PathSet, error) { + pathSet := graph.NewPathSet() + + if gpLinks, err := getGPOLinks(tx, node); err != nil { + return nil, err + } else { + for _, rel := range gpLinks { + if _, end, err := ops.FetchRelationshipNodes(tx, rel); err != nil { + return nil, err + } else { + // Not bothering with enforcement status here since inheritance does not affect Sites + // We only want Sites here. We won't traverse further down. + if end.Kinds.ContainsOneOf(ad.Site) { + pathSet.AddPath(graph.Path{ + Nodes: []*graph.Node{node, end}, + Edges: []*graph.Relationship{rel}, + }) + } + } + } + return pathSet, nil + } +} + +func FetchGPOAffectedSites(tx graph.Transaction, node *graph.Node, skip, limit int) (graph.NodeSet, error) { + var ( + nodeSet = graph.NewNodeSet() + seenSites = graph.NewNodeSet() + limitSkipTracker = ops.LimitSkipTracker{ + Limit: limit, + Skip: skip, + } + ) + + if gpLinks, err := getGPOLinks(tx, node); err != nil { + return nil, err + } else { + for _, rel := range gpLinks { + if limitSkipTracker.AtLimit() { + break + } else if _, end, err := ops.FetchRelationshipNodes(tx, rel); err != nil { + return nil, err + } else if end.Kinds.ContainsOneOf(ad.Site) && seenSites.AddIfNotExists(end) && limitSkipTracker.ShouldCollect() { + nodeSet.Add(end) + } + } + + return nodeSet, nil + } +} + func CreateGPOAffectedIntermediariesPathDelegate(targetKinds ...graph.Kind) PathDelegate { return func(tx graph.Transaction, node *graph.Node) (graph.PathSet, error) { pathSet := graph.NewPathSet() @@ -426,7 +481,7 @@ func FetchEnforcedGPOs(tx graph.Transaction, target *graph.Node, skip, limit int Direction: graph.DirectionInbound, BranchQuery: func() graph.Criteria { return query.And( - query.KindIn(query.Start(), ad.Domain, ad.OU, ad.GPO), + query.KindIn(query.Start(), ad.Domain, ad.OU, ad.Site, ad.GPO), query.KindIn(query.Relationship(), ad.Contains, ad.GPLink), ) }, @@ -451,8 +506,8 @@ func FetchEnforcedGPOs(tx graph.Transaction, target *graph.Node, skip, limit int // Walk the GPO path to see if any of the nodes between the GPO and the enforcement target block GPO // inheritance. This walk starts at the GPO and moves down, with end being the GPO to start segment.Path().WalkReverse(func(start, end *graph.Node, relationship *graph.Relationship) bool { - if !start.Kinds.ContainsOneOf(ad.OU, ad.Domain) { - // If we run into anything that isn't an OU or a Domain node then we're done checking for + if !start.Kinds.ContainsOneOf(ad.OU, ad.Domain, ad.Site) { + // If we run into anything that isn't an OU, Domain, or Site node then we're done checking for // inheritance blocking return false } else if lastNodeBlocks && start.Kinds.ContainsOneOf(ad.OU) { @@ -503,7 +558,7 @@ func FetchEnforcedGPOsPaths(ctx context.Context, db graph.Database, target *grap Direction: graph.DirectionInbound, BranchQuery: func() graph.Criteria { return query.And( - query.KindIn(query.Start(), ad.Domain, ad.OU, ad.GPO), + query.KindIn(query.Start(), ad.Domain, ad.OU, ad.Site, ad.GPO), query.KindIn(query.Relationship(), ad.Contains, ad.GPLink), ) }, @@ -527,8 +582,8 @@ func FetchEnforcedGPOsPaths(ctx context.Context, db graph.Database, target *grap // Walk the GPO path to see if any of the nodes between the GPO and the enforcement target block GPO // inheritance. This walk starts at the GPO and moves down, with end being the GPO to start segment.Path().WalkReverse(func(start, end *graph.Node, relationship *graph.Relationship) bool { - if !start.Kinds.ContainsOneOf(ad.OU, ad.Domain) { - // If we run into anything that isn't an OU or a Domain node then we're done checking for + if !start.Kinds.ContainsOneOf(ad.OU, ad.Domain, ad.Site) { + // If we run into anything that isn't an OU, Domain, or Site node then we're done checking for // inheritance blocking return false } else if lastNodeBlocks && start.Kinds.ContainsOneOf(ad.OU) { @@ -585,7 +640,7 @@ func FetchACLInheritancePath(ctx context.Context, db graph.Database, edge *graph Direction: graph.DirectionInbound, BranchQuery: func() graph.Criteria { return query.And( - query.KindIn(query.Start(), ad.Domain, ad.OU, ad.Container), + query.KindIn(query.Start(), ad.Domain, ad.OU, ad.Site, ad.Container), query.KindIn(query.Relationship(), ad.Contains), ) }, @@ -701,6 +756,42 @@ func CreateOUContainedPathDelegate(kind graph.Kind) PathDelegate { } } +func CreateSiteContainedListDelegate(kind graph.Kind) ListDelegate { + return func(tx graph.Transaction, node *graph.Node, skip, limit int) (graph.NodeSet, error) { + return ops.AcyclicTraverseTerminals(tx, ops.TraversalPlan{ + Root: node, + Direction: graph.DirectionOutbound, + BranchQuery: func() graph.Criteria { + return query.And( + query.Kind(query.Relationship(), ad.Contains), + ) + }, + PathFilter: func(ctx *ops.TraversalContext, segment *graph.PathSegment) bool { + return segment.Node.Kinds.ContainsOneOf(kind) + }, + Skip: skip, + Limit: limit, + }) + } +} + +func CreateSiteContainedPathDelegate(kind graph.Kind) PathDelegate { + return func(tx graph.Transaction, node *graph.Node) (graph.PathSet, error) { + return ops.TraversePaths(tx, ops.TraversalPlan{ + Root: node, + Direction: graph.DirectionOutbound, + BranchQuery: func() graph.Criteria { + return query.And( + query.Kind(query.Relationship(), ad.Contains), + ) + }, + PathFilter: func(ctx *ops.TraversalContext, segment *graph.PathSegment) bool { + return segment.Node.Kinds.ContainsOneOf(kind) + }, + }) + } +} + func CreateDomainTrustListDelegate(direction graph.Direction) ListDelegate { return func(tx graph.Transaction, node *graph.Node, skip, limit int) (graph.NodeSet, error) { return ops.AcyclicTraverseNodes(tx, ops.TraversalPlan{ diff --git a/packages/go/ein/ad.go b/packages/go/ein/ad.go index 71cde9cd021e..1a77cff5e516 100644 --- a/packages/go/ein/ad.go +++ b/packages/go/ein/ad.go @@ -94,6 +94,20 @@ func ConvertContainerToNode(item Container, ingestTime time.Time) IngestibleNode } } +func ConvertSiteToNode(item Site, ingestTime time.Time) IngestibleNode { + itemProps := getBaseProperties(item.IngestBase, ingestTime) + + if len(item.InheritanceHashes) > 0 { + itemProps[ad.InheritanceHashes.String()] = item.InheritanceHashes + } + + return IngestibleNode{ + ObjectID: item.ObjectIdentifier, + PropertyMap: itemProps, + Labels: []graph.Kind{ad.Site}, + } +} + func ConvertComputerToNode(item Computer, ingestTime time.Time) IngestibleNode { itemProps := getBaseProperties(item.IngestBase, ingestTime) @@ -782,6 +796,29 @@ func ParseChildObjects(data []TypedPrincipal, containerId string, containerType return relationships } +func ParseSiteServerData(siteServer SiteServer) []IngestibleRelationship { + if siteServer.ServerIs.ObjectIdentifier == "" { + return nil + } + + return []IngestibleRelationship{ + NewIngestibleRelationship( + IngestibleEndpoint{ + Value: siteServer.ObjectIdentifier, + Kind: ad.SiteServer, + }, + IngestibleEndpoint{ + Value: siteServer.ServerIs.ObjectIdentifier, + Kind: siteServer.ServerIs.Kind(), + }, + IngestibleRel{ + RelProps: map[string]any{ad.IsACL.String(): false}, + RelType: ad.ServerIs, + }, + ), + } +} + func ParseGPOChanges(changes GPOChanges) ParsedLocalGroupData { parsedData := ParsedLocalGroupData{} diff --git a/packages/go/ein/ad_test.go b/packages/go/ein/ad_test.go index 42f88d272f1f..c2e9d216d3fe 100644 --- a/packages/go/ein/ad_test.go +++ b/packages/go/ein/ad_test.go @@ -129,6 +129,36 @@ func TestConvertContainerToNode_InheritanceHashes(t *testing.T) { assert.Contains(t, result.PropertyMap[ad.InheritanceHashes.String()], testHash) } +func TestConvertSiteToNode_InheritanceHashes(t *testing.T) { + testHash := "abc123" + siteObject := ein.Site{ + IngestBase: ein.IngestBase{}, + InheritanceHashes: []string{testHash}, + } + + result := ein.ConvertSiteToNode(siteObject, time.Now().UTC()) + assert.Contains(t, result.PropertyMap[ad.InheritanceHashes.String()], testHash) +} + +func TestParseSiteServerData_ServerIs(t *testing.T) { + siteServer := ein.SiteServer{ + IngestBase: ein.IngestBase{ + ObjectIdentifier: "S-1-5-21-123-456-789-1001", + }, + ServerIs: ein.TypedPrincipal{ + ObjectIdentifier: "S-1-5-21-123-456-789-1002", + ObjectType: "Computer", + }, + } + + result := ein.ParseSiteServerData(siteServer) + require.Len(t, result, 1) + assert.Equal(t, ad.ServerIs, result[0].RelType) + assert.Equal(t, siteServer.ObjectIdentifier, result[0].Source.Value) + assert.Equal(t, siteServer.ServerIs.ObjectIdentifier, result[0].Target.Value) + assert.Equal(t, ad.Computer, result[0].Target.Kind) +} + func TestParseDomainTrusts_TrustAttributes(t *testing.T) { domainObject := ein.Domain{ IngestBase: ein.IngestBase{}, diff --git a/packages/go/ein/incoming_models.go b/packages/go/ein/incoming_models.go index 898948100319..73353bfd6137 100644 --- a/packages/go/ein/incoming_models.go +++ b/packages/go/ein/incoming_models.go @@ -173,6 +173,20 @@ type IssuancePolicy struct { GroupLink TypedPrincipal } +type Site struct { + IngestBase + ChildObjects []TypedPrincipal + Links []GPLink + InheritanceHashes []string +} + +type SiteServer struct { + IngestBase + ServerIs TypedPrincipal +} + +type SiteSubnet IngestBase + type RootCA struct { IngestBase DomainSID string diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index c1873329312b..20dd37f68edf 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -41,6 +41,9 @@ var ( NTAuthStore = graph.StringKind("NTAuthStore") CertTemplate = graph.StringKind("CertTemplate") IssuancePolicy = graph.StringKind("IssuancePolicy") + Site = graph.StringKind("Site") + SiteServer = graph.StringKind("SiteServer") + SiteSubnet = graph.StringKind("SiteSubnet") Owns = graph.StringKind("Owns") GenericAll = graph.StringKind("GenericAll") GenericWrite = graph.StringKind("GenericWrite") @@ -52,6 +55,7 @@ var ( AddMember = graph.StringKind("AddMember") HasSession = graph.StringKind("HasSession") Contains = graph.StringKind("Contains") + ServerIs = graph.StringKind("ServerIs") GPLink = graph.StringKind("GPLink") AllowedToDelegate = graph.StringKind("AllowedToDelegate") CoerceToTGT = graph.StringKind("CoerceToTGT") @@ -272,12 +276,13 @@ const ( NetBIOS Property = "netbios" AdminSDHolderProtected Property = "adminsdholderprotected" ServicePrincipalNames Property = "serviceprincipalnames" + Serverreference Property = "serverreference" GPOStatusRaw Property = "gpostatusraw" GPOStatus Property = "gpostatus" ) func AllProperties() []Property { - return []Property{AdminCount, CASecurityCollected, CAName, CertChain, CertName, CertThumbprint, CertThumbprints, HasEnrollmentAgentRestrictions, EnrollmentAgentRestrictionsCollected, IsUserSpecifiesSanEnabled, IsUserSpecifiesSanEnabledCollected, RoleSeparationEnabled, RoleSeparationEnabledCollected, HasBasicConstraints, BasicConstraintPathLength, UnresolvedPublishedTemplates, DNSHostname, CrossCertificatePair, DistinguishedName, DomainFQDN, DomainSID, Sensitive, BlocksInheritance, IsACL, IsACLProtected, InheritanceHash, InheritanceHashes, IsDeleted, Enforced, Department, HasCrossCertificatePair, HasSPN, UnconstrainedDelegation, LastLogon, LastLogonTimestamp, IsPrimaryGroup, HasLAPS, DontRequirePreAuth, LogonType, HasURA, PasswordNeverExpires, PasswordNotRequired, FunctionalLevel, TrustType, SpoofSIDHistoryBlocked, TrustedToAuth, SamAccountName, CertificateMappingMethodsRaw, CertificateMappingMethods, StrongCertificateBindingEnforcementRaw, StrongCertificateBindingEnforcement, VulnerableNetlogonSecurityDescriptor, VulnerableNetlogonSecurityDescriptorCollected, EKUs, SubjectAltRequireUPN, SubjectAltRequireDNS, SubjectAltRequireDomainDNS, SubjectAltRequireEmail, SubjectAltRequireSPN, SubjectRequireEmail, AuthorizedSignatures, ApplicationPolicies, IssuancePolicies, SchemaVersion, RequiresManagerApproval, AuthenticationEnabled, SchannelAuthenticationEnabled, EnrolleeSuppliesSubject, CertificateApplicationPolicy, CertificateNameFlag, EffectiveEKUs, EnrollmentFlag, Flags, NoSecurityExtension, RenewalPeriod, ValidityPeriod, OID, HomeDirectory, CertificatePolicy, CertTemplateOID, GroupLinkID, ObjectGUID, ExpirePasswordsOnSmartCardOnlyAccounts, MachineAccountQuota, SupportedKerberosEncryptionTypes, TGTDelegation, PasswordStoredUsingReversibleEncryption, SmartcardRequired, UseDESKeyOnly, LogonScriptEnabled, LockedOut, UserCannotChangePassword, PasswordExpired, DSHeuristics, UserAccountControl, TrustAttributesInbound, TrustAttributesOutbound, MinPwdLength, PwdProperties, PwdHistoryLength, LockoutThreshold, MinPwdAge, MaxPwdAge, LockoutDuration, LockoutObservationWindow, OwnerSid, SMBSigning, WebClientRunning, RestrictOutboundNTLM, GMSA, MSA, DoesAnyAceGrantOwnerRights, DoesAnyInheritedAceGrantOwnerRights, ADCSWebEnrollmentHTTP, ADCSWebEnrollmentHTTPS, ADCSWebEnrollmentHTTPSEPA, LDAPSigning, LDAPAvailable, LDAPSAvailable, LDAPSEPA, IsDC, IsReadOnlyDC, HTTPEnrollmentEndpoints, HTTPSEnrollmentEndpoints, HasVulnerableEndpoint, RequireSecuritySignature, EnableSecuritySignature, RestrictReceivingNTLMTraffic, NTLMMinServerSec, NTLMMinClientSec, LMCompatibilityLevel, UseMachineID, ClientAllowedNTLMServers, Transitive, GroupScope, NetBIOS, AdminSDHolderProtected, ServicePrincipalNames, GPOStatusRaw, GPOStatus} + return []Property{AdminCount, CASecurityCollected, CAName, CertChain, CertName, CertThumbprint, CertThumbprints, HasEnrollmentAgentRestrictions, EnrollmentAgentRestrictionsCollected, IsUserSpecifiesSanEnabled, IsUserSpecifiesSanEnabledCollected, RoleSeparationEnabled, RoleSeparationEnabledCollected, HasBasicConstraints, BasicConstraintPathLength, UnresolvedPublishedTemplates, DNSHostname, CrossCertificatePair, DistinguishedName, DomainFQDN, DomainSID, Sensitive, BlocksInheritance, IsACL, IsACLProtected, InheritanceHash, InheritanceHashes, IsDeleted, Enforced, Department, HasCrossCertificatePair, HasSPN, UnconstrainedDelegation, LastLogon, LastLogonTimestamp, IsPrimaryGroup, HasLAPS, DontRequirePreAuth, LogonType, HasURA, PasswordNeverExpires, PasswordNotRequired, FunctionalLevel, TrustType, SpoofSIDHistoryBlocked, TrustedToAuth, SamAccountName, CertificateMappingMethodsRaw, CertificateMappingMethods, StrongCertificateBindingEnforcementRaw, StrongCertificateBindingEnforcement, VulnerableNetlogonSecurityDescriptor, VulnerableNetlogonSecurityDescriptorCollected, EKUs, SubjectAltRequireUPN, SubjectAltRequireDNS, SubjectAltRequireDomainDNS, SubjectAltRequireEmail, SubjectAltRequireSPN, SubjectRequireEmail, AuthorizedSignatures, ApplicationPolicies, IssuancePolicies, SchemaVersion, RequiresManagerApproval, AuthenticationEnabled, SchannelAuthenticationEnabled, EnrolleeSuppliesSubject, CertificateApplicationPolicy, CertificateNameFlag, EffectiveEKUs, EnrollmentFlag, Flags, NoSecurityExtension, RenewalPeriod, ValidityPeriod, OID, HomeDirectory, CertificatePolicy, CertTemplateOID, GroupLinkID, ObjectGUID, ExpirePasswordsOnSmartCardOnlyAccounts, MachineAccountQuota, SupportedKerberosEncryptionTypes, TGTDelegation, PasswordStoredUsingReversibleEncryption, SmartcardRequired, UseDESKeyOnly, LogonScriptEnabled, LockedOut, UserCannotChangePassword, PasswordExpired, DSHeuristics, UserAccountControl, TrustAttributesInbound, TrustAttributesOutbound, MinPwdLength, PwdProperties, PwdHistoryLength, LockoutThreshold, MinPwdAge, MaxPwdAge, LockoutDuration, LockoutObservationWindow, OwnerSid, SMBSigning, WebClientRunning, RestrictOutboundNTLM, GMSA, MSA, DoesAnyAceGrantOwnerRights, DoesAnyInheritedAceGrantOwnerRights, ADCSWebEnrollmentHTTP, ADCSWebEnrollmentHTTPS, ADCSWebEnrollmentHTTPSEPA, LDAPSigning, LDAPAvailable, LDAPSAvailable, LDAPSEPA, IsDC, IsReadOnlyDC, HTTPEnrollmentEndpoints, HTTPSEnrollmentEndpoints, HasVulnerableEndpoint, RequireSecuritySignature, EnableSecuritySignature, RestrictReceivingNTLMTraffic, NTLMMinServerSec, NTLMMinClientSec, LMCompatibilityLevel, UseMachineID, ClientAllowedNTLMServers, Transitive, GroupScope, NetBIOS, AdminSDHolderProtected, ServicePrincipalNames, Serverreference, GPOStatusRaw, GPOStatus} } func ParseProperty(source string) (Property, error) { switch source { @@ -557,6 +562,8 @@ func ParseProperty(source string) (Property, error) { return AdminSDHolderProtected, nil case "serviceprincipalnames": return ServicePrincipalNames, nil + case "serverreference": + return Serverreference, nil case "gpostatusraw": return GPOStatusRaw, nil case "gpostatus": @@ -843,6 +850,8 @@ func (s Property) String() string { return string(AdminSDHolderProtected) case ServicePrincipalNames: return string(ServicePrincipalNames) + case Serverreference: + return string(Serverreference) case GPOStatusRaw: return string(GPOStatusRaw) case GPOStatus: @@ -1129,6 +1138,8 @@ func (s Property) Name() string { return "AdminSDHolder Protected" case ServicePrincipalNames: return "Service Principal Names" + case Serverreference: + return "Server Reference" case GPOStatusRaw: return "GPO Status (Raw)" case GPOStatus: @@ -1146,10 +1157,10 @@ func (s Property) Is(others ...graph.Kind) bool { return false } func Nodes() []graph.Kind { - return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate, IssuancePolicy} + return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate, IssuancePolicy, Site, SiteServer, SiteSubnet} } func Relationships() []graph.Kind { - return []graph.Kind{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, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, WriteOwnerRaw, OwnsLimitedRights, OwnsRaw, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ProtectAdminGroups} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, Contains, ServerIs, 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, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, WriteOwnerRaw, OwnsLimitedRights, OwnsRaw, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ProtectAdminGroups} } func ACLRelationships() []graph.Kind { return []graph.Kind{AllExtendedRights, ForceChangePassword, AddMember, AddAllowedToAct, GenericAll, WriteDACL, WriteOwner, GenericWrite, ReadLAPSPassword, ReadGMSAPassword, Owns, AddSelf, WriteSPN, AddKeyCredentialLink, GetChanges, GetChangesAll, GetChangesInFilteredSet, WriteAccountRestrictions, WriteGPLink, SyncLAPSPassword, DCSync, ManageCertificates, ManageCA, Enroll, WritePKIEnrollmentFlag, WritePKINameFlag, WriteOwnerLimitedRights, OwnsLimitedRights, WriteAltSecurityIdentities, WritePublicInformation} @@ -1158,16 +1169,16 @@ func IngestACLRelationships() []graph.Kind { return []graph.Kind{AllExtendedRights, ForceChangePassword, AddMember, AddAllowedToAct, GenericAll, WriteDACL, GenericWrite, ReadLAPSPassword, ReadGMSAPassword, AddSelf, WriteSPN, AddKeyCredentialLink, GetChanges, GetChangesAll, GetChangesInFilteredSet, WriteAccountRestrictions, WriteGPLink, ManageCertificates, ManageCA, Enroll, WritePKIEnrollmentFlag, WritePKINameFlag, WriteOwnerLimitedRights, OwnsLimitedRights, WriteAltSecurityIdentities, WritePublicInformation} } func PathfindingRelationships() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, ServerIs, Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation} } func PathfindingRelationshipsMatchFrontend() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation, ProtectAdminGroups} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, ServerIs, Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation, ProtectAdminGroups} } func InboundRelationshipKinds() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, Contains} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, ServerIs, Contains} } func OutboundRelationshipKinds() []graph.Kind { - return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, Contains, DCFor} + return []graph.Kind{Owns, GenericAll, GenericWrite, WriteOwner, WriteDACL, MemberOf, ForceChangePassword, AllExtendedRights, AddMember, HasSession, GPLink, AllowedToDelegate, CoerceToTGT, AllowedToAct, AdminTo, CanPSRemote, CanRDP, ExecuteDCOM, HasSIDHistory, AddSelf, DCSync, ReadLAPSPassword, ReadGMSAPassword, DumpSMSAPassword, SQLAdmin, AddAllowedToAct, WriteSPN, AddKeyCredentialLink, SyncLAPSPassword, WriteAccountRestrictions, WriteGPLink, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC9a, ADCSESC9b, ADCSESC10a, ADCSESC10b, ADCSESC13, SyncedToADUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, WriteAltSecurityIdentities, WritePublicInformation, ManageCA, ManageCertificates, ServerIs, Contains, DCFor} } func PostProcessedRelationships() []graph.Kind { return []graph.Kind{DCSync, ProtectAdminGroups, SyncLAPSPassword, CanRDP, AdminTo, CanPSRemote, ExecuteDCOM, TrustedForNTAuth, IssuedSignedBy, EnterpriseCAFor, GoldenCert, ADCSESC1, ADCSESC3, ADCSESC4, ADCSESC6a, ADCSESC6b, ADCSESC10a, ADCSESC10b, ADCSESC9a, ADCSESC9b, ADCSESC13, EnrollOnBehalfOf, SyncedToADUser, ExtendedByPolicy, CoerceAndRelayNTLMToADCS, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, GPOAppliesTo, CanApplyGPO, HasTrustKeys} @@ -1181,5 +1192,5 @@ func IsACLKind(s graph.Kind) bool { return false } func NodeKinds() []graph.Kind { - return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate, IssuancePolicy} + return []graph.Kind{Entity, User, Computer, Group, GPO, OU, Container, Domain, LocalGroup, LocalUser, AIACA, RootCA, EnterpriseCA, NTAuthStore, CertTemplate, IssuancePolicy, Site, SiteServer, SiteSubnet} } diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index 58be06fdf43b..5dd7437880b9 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -40,10 +40,10 @@ func NodeKinds() []graph.Kind { return []graph.Kind{MigrationData} } func InboundRelationshipKinds() []graph.Kind { - return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToADUser, ad.CoerceAndRelayNTLMToSMB, ad.CoerceAndRelayNTLMToADCS, ad.WriteOwnerLimitedRights, ad.OwnsLimitedRights, ad.ClaimSpecialIdentity, ad.CoerceAndRelayNTLMToLDAP, ad.CoerceAndRelayNTLMToLDAPS, ad.ContainsIdentity, ad.PropagatesACEsTo, ad.GPOAppliesTo, ad.CanApplyGPO, ad.HasTrustKeys, ad.WriteAltSecurityIdentities, ad.WritePublicInformation, ad.ManageCA, ad.ManageCertificates, ad.Contains, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToEntraUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains, azure.AZAuthenticatesTo} + return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToADUser, ad.CoerceAndRelayNTLMToSMB, ad.CoerceAndRelayNTLMToADCS, ad.WriteOwnerLimitedRights, ad.OwnsLimitedRights, ad.ClaimSpecialIdentity, ad.CoerceAndRelayNTLMToLDAP, ad.CoerceAndRelayNTLMToLDAPS, ad.ContainsIdentity, ad.PropagatesACEsTo, ad.GPOAppliesTo, ad.CanApplyGPO, ad.HasTrustKeys, ad.WriteAltSecurityIdentities, ad.WritePublicInformation, ad.ManageCA, ad.ManageCertificates, ad.ServerIs, ad.Contains, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToEntraUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains, azure.AZAuthenticatesTo} } func OutboundRelationshipKinds() []graph.Kind { - return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToADUser, ad.CoerceAndRelayNTLMToSMB, ad.CoerceAndRelayNTLMToADCS, ad.WriteOwnerLimitedRights, ad.OwnsLimitedRights, ad.ClaimSpecialIdentity, ad.CoerceAndRelayNTLMToLDAP, ad.CoerceAndRelayNTLMToLDAPS, ad.ContainsIdentity, ad.PropagatesACEsTo, ad.GPOAppliesTo, ad.CanApplyGPO, ad.HasTrustKeys, ad.WriteAltSecurityIdentities, ad.WritePublicInformation, ad.ManageCA, ad.ManageCertificates, ad.Contains, ad.DCFor, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToEntraUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains, azure.AZAuthenticatesTo} + return []graph.Kind{ad.Owns, ad.GenericAll, ad.GenericWrite, ad.WriteOwner, ad.WriteDACL, ad.MemberOf, ad.ForceChangePassword, ad.AllExtendedRights, ad.AddMember, ad.HasSession, ad.GPLink, ad.AllowedToDelegate, ad.CoerceToTGT, ad.AllowedToAct, ad.AdminTo, ad.CanPSRemote, ad.CanRDP, ad.ExecuteDCOM, ad.HasSIDHistory, ad.AddSelf, ad.DCSync, ad.ReadLAPSPassword, ad.ReadGMSAPassword, ad.DumpSMSAPassword, ad.SQLAdmin, ad.AddAllowedToAct, ad.WriteSPN, ad.AddKeyCredentialLink, ad.SyncLAPSPassword, ad.WriteAccountRestrictions, ad.WriteGPLink, ad.GoldenCert, ad.ADCSESC1, ad.ADCSESC3, ad.ADCSESC4, ad.ADCSESC6a, ad.ADCSESC6b, ad.ADCSESC9a, ad.ADCSESC9b, ad.ADCSESC10a, ad.ADCSESC10b, ad.ADCSESC13, ad.SyncedToADUser, ad.CoerceAndRelayNTLMToSMB, ad.CoerceAndRelayNTLMToADCS, ad.WriteOwnerLimitedRights, ad.OwnsLimitedRights, ad.ClaimSpecialIdentity, ad.CoerceAndRelayNTLMToLDAP, ad.CoerceAndRelayNTLMToLDAPS, ad.ContainsIdentity, ad.PropagatesACEsTo, ad.GPOAppliesTo, ad.CanApplyGPO, ad.HasTrustKeys, ad.WriteAltSecurityIdentities, ad.WritePublicInformation, ad.ManageCA, ad.ManageCertificates, ad.ServerIs, ad.Contains, ad.DCFor, azure.AvereContributor, azure.Contributor, azure.GetCertificates, azure.GetKeys, azure.GetSecrets, azure.HasRole, azure.MemberOf, azure.Owner, azure.RunsAs, azure.VMContributor, azure.AutomationContributor, azure.KeyVaultContributor, azure.VMAdminLogin, azure.AddMembers, azure.AddSecret, azure.ExecuteCommand, azure.GlobalAdmin, azure.PrivilegedAuthAdmin, azure.Grant, azure.GrantSelf, azure.PrivilegedRoleAdmin, azure.ResetPassword, azure.UserAccessAdministrator, azure.Owns, azure.CloudAppAdmin, azure.AppAdmin, azure.AddOwner, azure.ManagedIdentity, azure.AKSContributor, azure.NodeResourceGroup, azure.WebsiteContributor, azure.LogicAppContributor, azure.AZMGAddMember, azure.AZMGAddOwner, azure.AZMGAddSecret, azure.AZMGGrantAppRoles, azure.AZMGGrantRole, azure.SyncedToEntraUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains, azure.AZAuthenticatesTo} } type Property string diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 0b9e449d70f5..d5db50bb5ff5 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -11171,6 +11171,60 @@ } } }, + "/api/v2/gpos/{object_id}/sites": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetGpoEntitySites", + "summary": "Get GPO entity sites", + "description": "Get a list, graph, or count of the sites affected by this GPO.", + "tags": [ + "GPOs", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.entity.skip" + }, + { + "$ref": "#/components/parameters/query.entity.limit" + }, + { + "$ref": "#/components/parameters/query.entity.type" + }, + { + "$ref": "#/components/parameters/query.entity.sort-by" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/related-entity-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, "/api/v2/gpos/{object_id}/tier-zero": { "parameters": [ { @@ -12116,6 +12170,366 @@ } } }, + "/api/v2/sites/{object_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteEntity", + "summary": "Get Site entity info", + "description": "Get info and counts for this Site node.", + "tags": [ + "Sites", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.hydrate-counts" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/entity-info-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not-found" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, + "/api/v2/sites/{object_id}/controllers": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteEntityControllers", + "summary": "Get Site entity controllers", + "description": "Get a list, graph, or count of the principals that can control this Site.", + "tags": [ + "Sites", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.entity.skip" + }, + { + "$ref": "#/components/parameters/query.entity.limit" + }, + { + "$ref": "#/components/parameters/query.entity.type" + }, + { + "$ref": "#/components/parameters/query.entity.sort-by" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/related-entity-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, + "/api/v2/sites/{object_id}/linked-gpos": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteEntityLinkedGpos", + "summary": "Get Site entity linked GPOs", + "description": "Get a list, graph, or count of the GPOs linked to this Site.", + "tags": [ + "Sites", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.entity.skip" + }, + { + "$ref": "#/components/parameters/query.entity.limit" + }, + { + "$ref": "#/components/parameters/query.entity.type" + }, + { + "$ref": "#/components/parameters/query.entity.sort-by" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/related-entity-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, + "/api/v2/sites/{object_id}/siteservers": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteEntitySiteServers", + "summary": "Get Site entity site servers", + "description": "Get a list, graph, or count of the site servers linked to this Site.", + "tags": [ + "Sites", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.entity.skip" + }, + { + "$ref": "#/components/parameters/query.entity.limit" + }, + { + "$ref": "#/components/parameters/query.entity.type" + }, + { + "$ref": "#/components/parameters/query.entity.sort-by" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/related-entity-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, + "/api/v2/sites/{object_id}/sitesubnets": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteEntitySiteSubnets", + "summary": "Get Site entity site subnets", + "description": "Get a list, graph, or count of the site subnets linked to this Site.", + "tags": [ + "Sites", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.entity.skip" + }, + { + "$ref": "#/components/parameters/query.entity.limit" + }, + { + "$ref": "#/components/parameters/query.entity.type" + }, + { + "$ref": "#/components/parameters/query.entity.sort-by" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/related-entity-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, + "/api/v2/siteservers/{object_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteServerEntity", + "summary": "Get Site Server entity info", + "description": "Get info and counts for this Site Server node.", + "tags": [ + "Site Servers", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.hydrate-counts" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/entity-info-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not-found" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, + "/api/v2/sitesubnets/{object_id}": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteSubnetEntity", + "summary": "Get Site Subnet entity info", + "description": "Get info and counts for this Site Subnet node.", + "tags": [ + "Site Subnets", + "Community", + "Enterprise" + ], + "parameters": [ + { + "$ref": "#/components/parameters/query.hydrate-counts" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/entity-info-query-results" + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not-found" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, "/api/v2/ous/{object_id}": { "parameters": [ { @@ -21767,6 +22181,18 @@ "certtemplates": { "type": "integer" }, + "issuancepolicies": { + "type": "integer" + }, + "sites": { + "type": "integer" + }, + "siteservers": { + "type": "integer" + }, + "sitesubnets": { + "type": "integer" + }, "acls": { "type": "integer" }, @@ -21895,6 +22321,18 @@ "certtemplates": { "type": "integer" }, + "issuancepolicies": { + "type": "integer" + }, + "sites": { + "type": "integer" + }, + "siteservers": { + "type": "integer" + }, + "sitesubnets": { + "type": "integer" + }, "acls": { "type": "integer" }, @@ -22970,6 +23408,9 @@ "Enterprise CAs", "NT Auth Stores", "Cert Templates", + "Sites", + "Site Servers", + "Site Subnets", "OUs", "AD Users", "Groups", diff --git a/packages/go/openapi/src/openapi.yaml b/packages/go/openapi/src/openapi.yaml index c1b5df1494bd..e1281ba586b1 100644 --- a/packages/go/openapi/src/openapi.yaml +++ b/packages/go/openapi/src/openapi.yaml @@ -174,6 +174,9 @@ x-tagGroups: - Enterprise CAs - NT Auth Stores - Cert Templates + - Sites + - Site Servers + - Site Subnets - OUs - AD Users - Groups @@ -501,6 +504,8 @@ paths: $ref: './paths/gpos.gpos.id.controllers.yaml' /api/v2/gpos/{object_id}/ous: $ref: './paths/gpos.gpos.id.ous.yaml' + /api/v2/gpos/{object_id}/sites: + $ref: './paths/gpos.gpos.id.sites.yaml' /api/v2/gpos/{object_id}/tier-zero: $ref: './paths/gpos.gpos.id.tier-zero.yaml' /api/v2/gpos/{object_id}/users: @@ -548,6 +553,26 @@ paths: /api/v2/certtemplates/{object_id}/published-to-cas: $ref: './paths/certtemplate.certtemplates.id.published-to-cas.yaml' + # sites + /api/v2/sites/{object_id}: + $ref: './paths/sites.sites.id.yaml' + /api/v2/sites/{object_id}/controllers: + $ref: './paths/sites.sites.id.controllers.yaml' + /api/v2/sites/{object_id}/linked-gpos: + $ref: './paths/sites.sites.id.linked-gpos.yaml' + /api/v2/sites/{object_id}/siteservers: + $ref: './paths/sites.sites.id.siteservers.yaml' + /api/v2/sites/{object_id}/sitesubnets: + $ref: './paths/sites.sites.id.sitesubnets.yaml' + + # site servers + /api/v2/siteservers/{object_id}: + $ref: './paths/siteservers.siteservers.id.yaml' + + # site subnets + /api/v2/sitesubnets/{object_id}: + $ref: './paths/sitesubnets.sitesubnets.id.yaml' + # ous /api/v2/ous/{object_id}: $ref: './paths/ous.ous.id.yaml' diff --git a/packages/go/openapi/src/paths/gpos.gpos.id.sites.yaml b/packages/go/openapi/src/paths/gpos.gpos.id.sites.yaml new file mode 100644 index 000000000000..a490a07b2d03 --- /dev/null +++ b/packages/go/openapi/src/paths/gpos.gpos.id.sites.yaml @@ -0,0 +1,45 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetGpoEntitySites + summary: Get GPO entity sites + description: Get a list, graph, or count of the sites affected by this GPO. + tags: + - GPOs + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.entity.skip.yaml' + - $ref: './../parameters/query.entity.limit.yaml' + - $ref: './../parameters/query.entity.type.yaml' + - $ref: './../parameters/query.entity.sort-by.yaml' + responses: + 200: + $ref: './../responses/related-entity-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/sites.sites.id.controllers.yaml b/packages/go/openapi/src/paths/sites.sites.id.controllers.yaml new file mode 100644 index 000000000000..3848b3b4ed25 --- /dev/null +++ b/packages/go/openapi/src/paths/sites.sites.id.controllers.yaml @@ -0,0 +1,45 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteEntityControllers + summary: Get Site entity controllers + description: Get a list, graph, or count of the principals that can control this Site. + tags: + - Sites + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.entity.skip.yaml' + - $ref: './../parameters/query.entity.limit.yaml' + - $ref: './../parameters/query.entity.type.yaml' + - $ref: './../parameters/query.entity.sort-by.yaml' + responses: + 200: + $ref: './../responses/related-entity-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/sites.sites.id.linked-gpos.yaml b/packages/go/openapi/src/paths/sites.sites.id.linked-gpos.yaml new file mode 100644 index 000000000000..e42339ff7b3d --- /dev/null +++ b/packages/go/openapi/src/paths/sites.sites.id.linked-gpos.yaml @@ -0,0 +1,45 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteEntityLinkedGpos + summary: Get Site entity linked GPOs + description: Get a list, graph, or count of the GPOs linked to this Site. + tags: + - Sites + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.entity.skip.yaml' + - $ref: './../parameters/query.entity.limit.yaml' + - $ref: './../parameters/query.entity.type.yaml' + - $ref: './../parameters/query.entity.sort-by.yaml' + responses: + 200: + $ref: './../responses/related-entity-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/sites.sites.id.siteservers.yaml b/packages/go/openapi/src/paths/sites.sites.id.siteservers.yaml new file mode 100644 index 000000000000..5b99f42a9013 --- /dev/null +++ b/packages/go/openapi/src/paths/sites.sites.id.siteservers.yaml @@ -0,0 +1,45 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteEntitySiteServers + summary: Get Site entity site servers + description: Get a list, graph, or count of the site servers linked to this Site. + tags: + - Sites + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.entity.skip.yaml' + - $ref: './../parameters/query.entity.limit.yaml' + - $ref: './../parameters/query.entity.type.yaml' + - $ref: './../parameters/query.entity.sort-by.yaml' + responses: + 200: + $ref: './../responses/related-entity-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/sites.sites.id.sitesubnets.yaml b/packages/go/openapi/src/paths/sites.sites.id.sitesubnets.yaml new file mode 100644 index 000000000000..91247a810afa --- /dev/null +++ b/packages/go/openapi/src/paths/sites.sites.id.sitesubnets.yaml @@ -0,0 +1,45 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteEntitySiteSubnets + summary: Get Site entity site subnets + description: Get a list, graph, or count of the site subnets linked to this Site. + tags: + - Sites + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.entity.skip.yaml' + - $ref: './../parameters/query.entity.limit.yaml' + - $ref: './../parameters/query.entity.type.yaml' + - $ref: './../parameters/query.entity.sort-by.yaml' + responses: + 200: + $ref: './../responses/related-entity-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/sites.sites.id.yaml b/packages/go/openapi/src/paths/sites.sites.id.yaml new file mode 100644 index 000000000000..f47e43454613 --- /dev/null +++ b/packages/go/openapi/src/paths/sites.sites.id.yaml @@ -0,0 +1,44 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteEntity + summary: Get Site entity info + description: Get info and counts for this Site node. + tags: + - Sites + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.hydrate-counts.yaml' + responses: + 200: + $ref: './../responses/entity-info-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 404: + $ref: './../responses/not-found.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/siteservers.siteservers.id.yaml b/packages/go/openapi/src/paths/siteservers.siteservers.id.yaml new file mode 100644 index 000000000000..cbb5025cc950 --- /dev/null +++ b/packages/go/openapi/src/paths/siteservers.siteservers.id.yaml @@ -0,0 +1,44 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteServerEntity + summary: Get Site Server entity info + description: Get info and counts for this Site Server node. + tags: + - Site Servers + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.hydrate-counts.yaml' + responses: + 200: + $ref: './../responses/entity-info-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 404: + $ref: './../responses/not-found.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.yaml b/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.yaml new file mode 100644 index 000000000000..6e88909dac37 --- /dev/null +++ b/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.yaml @@ -0,0 +1,44 @@ +# Copyright 2026 Specter Ops, Inc. +# +# Licensed under the Apache License, Version 2.0 +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - $ref: './../parameters/path.object-id.yaml' +get: + operationId: GetSiteSubnetEntity + summary: Get Site Subnet entity info + description: Get info and counts for this Site Subnet node. + tags: + - Site Subnets + - Community + - Enterprise + parameters: + - $ref: './../parameters/query.hydrate-counts.yaml' + responses: + 200: + $ref: './../responses/entity-info-query-results.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 404: + $ref: './../responses/not-found.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' diff --git a/packages/go/openapi/src/schemas/model.ad-data-quality-aggregation.yaml b/packages/go/openapi/src/schemas/model.ad-data-quality-aggregation.yaml index cd61a3f242ce..edadac313f73 100644 --- a/packages/go/openapi/src/schemas/model.ad-data-quality-aggregation.yaml +++ b/packages/go/openapi/src/schemas/model.ad-data-quality-aggregation.yaml @@ -43,6 +43,14 @@ allOf: type: integer certtemplates: type: integer + issuancepolicies: + type: integer + sites: + type: integer + siteservers: + type: integer + sitesubnets: + type: integer acls: type: integer sessions: diff --git a/packages/go/openapi/src/schemas/model.ad-data-quality-stat.yaml b/packages/go/openapi/src/schemas/model.ad-data-quality-stat.yaml index 0d6374f90f4d..e3892f2e04d7 100644 --- a/packages/go/openapi/src/schemas/model.ad-data-quality-stat.yaml +++ b/packages/go/openapi/src/schemas/model.ad-data-quality-stat.yaml @@ -43,6 +43,14 @@ allOf: type: integer certtemplates: type: integer + issuancepolicies: + type: integer + sites: + type: integer + siteservers: + type: integer + sitesubnets: + type: integer acls: type: integer sessions: diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 010658bc0443..2258ded12598 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -76,6 +76,18 @@ var nodeIcons = map[string]nodeIcon{ Icon: "clipboard-check", Color: "#99B2DD", }, + "Site": { + Icon: "map-signs", + Color: "#2DD4BF", + }, + "SiteServer": { + Icon: "map-marker", + Color: "#60A5FA", + }, + "SiteSubnet": { + Icon: "map", + Color: "#F59E0B", + }, "OU": { Icon: "sitemap", Color: "#FFAA00", @@ -193,7 +205,7 @@ BEGIN RETURN kind_id; END $$ LANGUAGE plpgsql; - `) +`) sb.WriteString(` CREATE OR REPLACE FUNCTION genscript_upsert_source_kind(kind_name TEXT) RETURNS SMALLINT AS $$ @@ -213,7 +225,7 @@ BEGIN RETURN source_kind_id; END $$ LANGUAGE plpgsql; - `) +`) 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 $$ diff --git a/packages/javascript/bh-shared-ui/src/commonSearchesAGI.ts b/packages/javascript/bh-shared-ui/src/commonSearchesAGI.ts index d8d8c516ac23..76cba5370a90 100644 --- a/packages/javascript/bh-shared-ui/src/commonSearchesAGI.ts +++ b/packages/javascript/bh-shared-ui/src/commonSearchesAGI.ts @@ -63,6 +63,37 @@ export const CommonSearches: CommonSearchType[] = [ }, ], }, + { + subheader: 'AD Sites', + category: categoryAD, + queries: [ + { + name: 'Map AD site topology', + description: 'Shows sites and the site-server or site-subnet objects they contain.', + query: `MATCH p = (:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site controllers', + description: 'Shows sites, their controllers, and the site-server or site-subnet objects they contain.', + query: `MATCH p = (:Base)-[:Owns|WriteOwner|WriteDacl|GenericAll|GenericWrite|WriteGPLink]->(:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site-linked GPOs', + description: 'Shows site-linked GPOs and affected site-servers and site-subnets', + query: `MATCH p = (:GPO)-[:GPLink]->(:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site-linked GPO controllers', + description: 'Shows site-linked GPO controllers and affected site-servers and site-subnets', + query: `MATCH p = (:Base)-[:Owns|WriteOwner|WriteDacl|GenericAll|GenericWrite]->(:GPO)-[:GPLink]->(:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site servers to computers', + description: 'Shows SiteServer nodes and the Computer objects they resolve to through ServerIs.', + query: `MATCH p = (:Site)-[:Contains]->(:SiteServer)-[:ServerIs]->(:Computer)\nRETURN p\nLIMIT 1000`, + }, + ], + }, { subheader: 'Dangerous Privileges', category: categoryAD, diff --git a/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts b/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts index 841d0d9eb0bd..9a5b2e7ef782 100644 --- a/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts +++ b/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts @@ -63,6 +63,37 @@ export const CommonSearches: CommonSearchType[] = [ }, ], }, + { + subheader: 'AD Sites', + category: categoryAD, + queries: [ + { + name: 'Map AD site topology', + description: 'Shows sites and the site-server or site-subnet objects they contain.', + query: `MATCH p = (:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site controllers', + description: 'Shows sites, their controllers, and the site-server or site-subnet objects they contain.', + query: `MATCH p = (:Base)-[:Owns|WriteOwner|WriteDacl|GenericAll|GenericWrite|WriteGPLink]->(:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site-linked GPOs', + description: 'Shows site-linked GPOs and affected site-servers and site-subnets', + query: `MATCH p = (:GPO)-[:GPLink]->(:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site-linked GPO controllers', + description: 'Shows site-linked GPO controllers and affected site-servers and site-subnets', + query: `MATCH p = (:Base)-[:Owns|WriteOwner|WriteDacl|GenericAll|GenericWrite]->(:GPO)-[:GPLink]->(:Site)-[:Contains]->(n:Base)\nWHERE n:SiteServer OR n:SiteSubnet\nRETURN p\nLIMIT 1000`, + }, + { + name: 'Map site servers to computers', + description: 'Shows SiteServer nodes and the Computer objects they resolve to through ServerIs.', + query: `MATCH p = (:Site)-[:Contains]->(:SiteServer)-[:ServerIs]->(:Computer)\nRETURN p\nLIMIT 1000`, + }, + ], + }, { subheader: 'Dangerous Privileges', category: categoryAD, diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/General.tsx index b91904ecb9dc..bdf770bf6c7c 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/General.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/General.tsx @@ -28,6 +28,17 @@ const General: FC = ({ sourceName, targetName, targetType }) => { A linked GPO applies its settings to objects in the linked container. + + For domain and OU objects, affected child users and computers include those contained directly within + the domain or OU, as well as those in nested OUs. However, unless the GPO link is enforced, some users + and computers may not be affected if GPO inheritance is blocked on a containing OU. + + + For site objects, affected computers include the site's domain controllers, and also computers whose IP + addresses fall within one of the site's subnets. If the site is the default site, affected computers + also include computers that do not map to any other site. Affected users are those who sign in to the + affected computers. + ); }; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/LinuxAbuse.tsx index a2a1f5bdd292..18a57af7ebad 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/LinuxAbuse.tsx @@ -23,18 +23,53 @@ const LinuxAbuse: FC = () => { return ( <> - With full control of a GPO, you may make modifications to that GPO which will then apply to the users - and computers affected by the GPO. Select the target object you wish to push an evil policy down to, - then use the gpedit GUI to modify the GPO, using an evil policy that allows item-level targeting, such - as a new immediate scheduled task. Then wait at least 2 hours for the group policy client to pick up and - execute the new evil policy. See the references tab for a more detailed write up on this abuse. + If you control a GPO linked to a target object, you can modify that GPO to inject malicious + configuration. For example, you can add an immediate scheduled task that runs on the computers or users + that process the GPO, compromising those objects. Some settings, including scheduled tasks, support + item-level targeting, which can limit execution to specific objects. GPOs apply every 90 minutes for + standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain controllers. + The{' '} + + GroupPolicyBackdoor.py + {' '} + tool can perform the attack from Linux. First, define a module file that describes the configuration to + inject. The example below defines a computer configuration with an immediate scheduled task that adds a + domain user as a local administrator. The filter limits the configuration to a specific target. + + + + {'[MODULECONFIG]\n' + + 'name = Scheduled Tasks\n' + + 'type = computer\n' + + '\n' + + '[MODULEOPTIONS]\n' + + 'task_type = immediate\n' + + 'program = cmd.exe\n' + + 'arguments = /c "net localgroup Administrators corp.com\\john /add"\n' + + '\n' + + '[MODULEFILTERS]\n' + + 'filters = [{ "operator": "AND", "type": "Computer Name", "value": "srv1.corp.com"}]'} + + + + Save this configuration as Scheduled_task_add.ini, then inject it into the target GPO with the 'inject' + command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + + Alternatively,{' '} pyGPOAbuse.py {' '} - can be used for that purpose. + can also be used for this purpose. ); diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/WindowsAbuse.tsx index be3ecf100b9d..7b52e4516f1f 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/WindowsAbuse.tsx @@ -14,6 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 +import { Link } from '@mui/material'; import { Typography } from 'doodle-ui'; import { FC } from 'react'; import { EdgeInfoProps } from '../index'; @@ -22,11 +23,21 @@ const WindowsAbuse: FC = () => { return ( <> - With full control of a GPO, you may make modifications to that GPO which will then apply to the users - and computers affected by the GPO. Select the target object you wish to push an evil policy down to, - then use the gpedit GUI to modify the GPO, using an evil policy that allows item-level targeting, such - as a new immediate scheduled task. Then wait for the group policy client to pick up and execute the new - evil policy. See the references tab for a more detailed write up on this abuse. + If you control a GPO linked to a target object, you can modify that GPO to inject malicious + configuration. For example, you can add an immediate scheduled task that runs on the computers or users + that process the GPO, compromising those objects. Some settings, including scheduled tasks, support + item-level targeting, which can limit execution to specific objects. GPOs apply every 90 minutes for + standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain controllers. + See the References tab for more detail. + + + + On a domain-joined Windows machine, you can edit GPOs with the native Group Policy Management Console + (GPMC). On a non-domain-joined Windows machine, use the{' '} + + DRSAT (Disconnected RSAT) + {' '} + tool. ); diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/LinuxAbuse.tsx index a646556cceb7..6e436d5e51a4 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/LinuxAbuse.tsx @@ -399,48 +399,63 @@ const LinuxAbuse: FC = ( - In such a situation, it may still be possible to exploit GenericAll permissions on a domain - object through an alternative attack vector. Indeed, with GenericAll permissions over a domain - object, you may make modifications to the gPLink attribute of the domain. The ability to alter - the gPLink attribute of a domain may allow an attacker to apply a malicious Group Policy Object - (GPO) to all of the domain user and computer objects (including the ones located in nested OUs). - This can be exploited to make said child objects execute arbitrary commands through an immediate - scheduled task, thus compromising them. + In this situation, GenericAll on the domain object may still be exploitable through gPLink. + GenericAll allows you to modify the domain's gPLink attribute, which can be abused to link a + malicious Group Policy Object (GPO) to the domain. The linked GPO applies to the domain's users + and computers, including those in nested OUs, and can force those child objects to execute + arbitrary commands, for example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + From Linux, you can use the{' '} OUned.py {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + tool to exploit this gPLink manipulation path. For requirements and implementation details, see{' '} - the article associated to the OUned.py tool + the accompanying OUned.py article . - - Be mindful of the number of users and computers that are in the given domain as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target domain object through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the domain with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "DC=corp,DC=com" -n "TARGETGPO"' + } - Alternatively, the ability to modify the gPLink attribute of a domain can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target domain - through its gPLink attribute, making all child users and computers apply the malicious GPO and - execute arbitrary commands. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -448,19 +463,58 @@ const LinuxAbuse: FC = ( return ( <> - With full control of a GPO, you may make modifications to that GPO which will then apply to the - users and computers affected by the GPO. Select the target object you wish to push an evil - policy down to, then use the gpedit GUI to modify the GPO, using an evil policy that allows - item-level targeting, such as a new immediate scheduled task. Then wait at least 2 hours for the - group policy client to pick up and execute the new evil policy. See the references tab for a - more detailed write up on this abuse. + GenericAll on a GPO allows you to modify that GPO and inject malicious configuration. For + example, you can add an immediate scheduled task that runs on the computers or users that + process the GPO, compromising those objects. Some settings, including scheduled tasks, support + item-level targeting, which can limit execution to specific objects. GPOs apply every 90 minutes + for standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain + controllers. See the References tab for more detail. + The{' '} + + GroupPolicyBackdoor.py + {' '} + tool can perform the attack from Linux. First, define a module file that describes the + configuration to inject. The example below defines a computer configuration with an immediate + scheduled task that adds a domain user as a local administrator. The filter limits the + configuration to a specific target. + + + + {'[MODULECONFIG]\n' + + 'name = Scheduled Tasks\n' + + 'type = computer\n' + + '\n' + + '[MODULEOPTIONS]\n' + + 'task_type = immediate\n' + + 'program = cmd.exe\n' + + 'arguments = /c "net localgroup Administrators corp.com\\john /add"\n' + + '\n' + + '[MODULEFILTERS]\n' + + 'filters = [{ "operator": "AND", "type": "Computer Name", "value": "srv1.corp.com"}]'} + + + + Save this configuration as Scheduled_task_add.ini, then inject it into the target GPO with the + 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + + Alternatively,{' '} pyGPOAbuse.py {' '} - can be used for that purpose. + can also be used for this purpose. ); @@ -503,55 +557,70 @@ const LinuxAbuse: FC = ( Objects for which ACL inheritance is disabled - It is important to note that the compromise vector described above relies on ACL inheritance and - will not work for objects with ACL inheritance disabled, such as objects protected by - AdminSDHolder (attribute adminCount=1). This observation applies to any OU child user or - computer with ACL inheritance disabled, including objects located in nested sub-OUs. + The compromise vector described above relies on ACL inheritance and will not work for objects + with ACL inheritance disabled, such as objects protected by AdminSDHolder (attribute + adminCount=1). This observation applies to any user or computer with inheritance disabled, + including objects located in nested OUs. - In such a situation, it may still be possible to exploit GenericAll permissions on an OU through - an alternative attack vector. Indeed, with GenericAll permissions over an OU, you may make - modifications to the gPLink attribute of the OU. The ability to alter the gPLink attribute of an - OU may allow an attacker to apply a malicious Group Policy Object (GPO) to all of the OU's child - user and computer objects (including the ones located in nested sub-OUs). This can be exploited - to make said child objects execute arbitrary commands through an immediate scheduled task, thus - compromising them. + In this situation, GenericAll on the OU may still be exploitable through gPLink. GenericAll + allows you to modify the OU's gPLink attribute, which can be abused to link a malicious Group + Policy Object (GPO) to the OU. The linked GPO applies to the OU's users and computers, including + those in nested OUs, and can force those child objects to execute arbitrary commands, for + example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + From Linux, you can use the{' '} OUned.py {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + tool to exploit this gPLink manipulation path. For requirements and implementation details, see{' '} - the article associated to the OUned.py tool + the accompanying OUned.py article . - - Be mindful of the number of users and computers that are in the given OU as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target OU through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the OU with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "OU=SERVERS,DC=corp,DC=com" -n "TARGETGPO"' + } - Alternatively, the ability to modify the gPLink attribute of an OU can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target OU through - its gPLink attribute, making all child users and computers apply the malicious GPO and execute - arbitrary commands. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -644,6 +713,71 @@ const LinuxAbuse: FC = ( ); + case 'Site': + return ( + <> + + GenericAll permissions on a site object allow you to modify its gPLink attribute. A malicious + Group Policy Object (GPO) linked to the site can force affected computers and users to execute + arbitrary commands, for example through an immediate scheduled task. + + + For site objects, affected computers include the site's domain controllers, and also computers + whose IP addresses fall within one of the site's subnets. If the site is the default site, + affected computers also include computers that do not map to any other site. Affected users are + those who sign in to the affected computers. + + + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From Linux, see{' '} + + the Site Unseen article + {' '} + for site-specific gPLink attack requirements and implementation details. + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target site object through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the site object with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=corp,DC=com" -n "TARGETGPO"' + } + + + + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); default: return ( diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/References.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/References.tsx index 557c4bdd74bf..bc76e6543b1b 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/References.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/References.tsx @@ -214,6 +214,13 @@ const References: FC = () => { https://github.com/CravateRouge/bloodyAD +
+ + https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites + ); }; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/WindowsAbuse.tsx index 74e11225bd6e..f079cd71122a 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericAll/WindowsAbuse.tsx @@ -570,26 +570,24 @@ const WindowsAbuse: FC = - In such a situation, it may still be possible to exploit GenericAll permissions on a domain - object through an alternative attack vector. Indeed, with GenericAll permissions over a domain - object, you may make modifications to the gPLink attribute of the domain. The ability to alter - the gPLink attribute of a domain may allow an attacker to apply a malicious Group Policy Object - (GPO) to all of the domain user and computer objects (including the ones located in nested OUs). - This can be exploited to make said child objects execute arbitrary commands through an immediate - scheduled task, thus compromising them. + In this situation, GenericAll on the domain object may still be exploitable through gPLink. + GenericAll allows you to modify the domain's gPLink attribute, which can be abused to link a + malicious Group Policy Object (GPO) to the domain. The linked GPO applies to the domain's users + and computers, including those in nested OUs, and can force those child objects to execute + arbitrary commands, for example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a domain-joined compromised Windows machine, the gPLink manipulation attack vector may be - exploited through Powermad, PowerView and native Windows functionalities. For a detailed outline - of exploit requirements and implementation, you can refer to{' '} + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For requirements and implementation + details, see{' '} = - Be mindful of the number of users and computers that are in the given domain as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target domain object through its gPLink attribute. - Alternatively, the ability to modify the gPLink attribute of a domain can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target domain - through its gPLink attribute, making all child users and computers apply the malicious GPO and - execute arbitrary commands. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -617,12 +613,21 @@ const WindowsAbuse: FC = return ( <> - With full control of a GPO, you may make modifications to that GPO which will then apply to the - users and computers affected by the GPO. Select the target object you wish to push an evil - policy down to, then use the gpedit GUI to modify the GPO, using an evil policy that allows - item-level targeting, such as a new immediate scheduled task. Then wait for the group policy - client to pick up and execute the new evil policy. See the references tab for a more detailed - write up on this abuse. + GenericAll on a GPO allows you to modify that GPO and inject malicious configuration. For + example, you can add an immediate scheduled task that runs on the computers or users that + process the GPO, compromising those objects. Some settings, including scheduled tasks, support + item-level targeting, which can limit execution to specific objects. GPOs apply every 90 minutes + for standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain + controllers. See the References tab for more detail. + + + + On a domain-joined Windows machine, you can edit GPOs with the native Group Policy Management + Console (GPMC). On a non-domain-joined Windows machine, use the{' '} + + DRSAT (Disconnected RSAT) + {' '} + tool. ); @@ -711,33 +716,31 @@ const WindowsAbuse: FC = Objects for which ACL inheritance is disabled - It is important to note that the compromise vector described above relies on ACL inheritance and - will not work for objects with ACL inheritance disabled, such as objects protected by - AdminSDHolder (attribute adminCount=1). This observation applies to any OU child user or - computer with ACL inheritance disabled, including objects located in nested sub-OUs. + The compromise vector described above relies on ACL inheritance and will not work for objects + with ACL inheritance disabled, such as objects protected by AdminSDHolder (attribute + adminCount=1). This observation applies to any user or computer with inheritance disabled, + including objects located in nested OUs. - In such a situation, it may still be possible to exploit GenericAll permissions on an OU through - an alternative attack vector. Indeed, with GenericAll permissions over an OU, you may make - modifications to the gPLink attribute of the OU. The ability to alter the gPLink attribute of an - OU may allow an attacker to apply a malicious Group Policy Object (GPO) to all of the OU's child - user and computer objects (including the ones located in nested sub-OUs). This can be exploited - to make said child objects execute arbitrary commands through an immediate scheduled task, thus - compromising them. + In this situation, GenericAll on the OU may still be exploitable through gPLink. GenericAll + allows you to modify the OU's gPLink attribute, which can be abused to link a malicious Group + Policy Object (GPO) to the OU. The linked GPO applies to the OU's users and computers, including + those in nested OUs, and can force those child objects to execute arbitrary commands, for + example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a domain-joined compromised Windows machine, the gPLink manipulation attack vector may be - exploited through Powermad, PowerView and native Windows functionalities. For a detailed outline - of exploit requirements and implementation, you can refer to{' '} + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For requirements and implementation + details, see{' '} = - Be mindful of the number of users and computers that are in the given OU as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target OU through its gPLink attribute. - Alternatively, the ability to modify the gPLink attribute of an OU can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target OU through - its gPLink attribute, making all child users and computers apply the malicious GPO and execute - arbitrary commands. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -844,6 +845,53 @@ const WindowsAbuse: FC = ); + case 'Site': + return ( + <> + + GenericAll permissions on a site object allow you to modify its gPLink attribute. A malicious + Group Policy Object (GPO) linked to the site can force affected computers and users to execute + arbitrary commands, for example through an immediate scheduled task.{' '} + + + For site objects, affected computers include the site's domain controllers, and also computers + whose IP addresses fall within one of the site's subnets. If the site is the default site, + affected computers also include computers that do not map to any other site. Affected users are + those who sign in to the affected computers. + + + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For site-specific requirements and + implementation details, see{' '} + + the Site Unseen article + + . + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target site object through its gPLink attribute. + + + + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); default: return ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/LinuxAbuse.tsx index fe38e4e3947c..a717c5ecc269 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/LinuxAbuse.tsx @@ -184,19 +184,58 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - With GenericWrite over a GPO, you may make modifications to that GPO which will then apply to - the users and computers affected by the GPO. Select the target object you wish to push an evil - policy down to, then use the gpedit GUI to modify the GPO, using an evil policy that allows - item-level targeting, such as a new immediate scheduled task. Then wait at least 2 hours for the - group policy client to pick up and execute the new evil policy. See the references tab for a - more detailed write up on this abuse. + GenericWrite on a GPO allows you to modify that GPO and inject malicious configuration. For + example, you can add an immediate scheduled task that runs on the computers or users that + process the GPO, compromising those objects. Some settings, including scheduled tasks, support + item-level targeting, which can limit execution to specific objects. GPOs apply every 90 minutes + for standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain + controllers. See the References tab for more detail. + The{' '} + + GroupPolicyBackdoor.py + {' '} + tool can perform the attack from Linux. First, define a module file that describes the + configuration to inject. The example below defines a computer configuration with an immediate + scheduled task that adds a domain user as a local administrator. The filter limits the + configuration to a specific target. + + + + {'[MODULECONFIG]\n' + + 'name = Scheduled Tasks\n' + + 'type = computer\n' + + '\n' + + '[MODULEOPTIONS]\n' + + 'task_type = immediate\n' + + 'program = cmd.exe\n' + + 'arguments = /c "net localgroup Administrators corp.com\\john /add"\n' + + '\n' + + '[MODULEFILTERS]\n' + + 'filters = [{ "operator": "AND", "type": "Computer Name", "value": "srv1.corp.com"}]'} + + + + Save this configuration as Scheduled_task_add.ini, then inject it into the target GPO with the + 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + + Alternatively,{' '} pyGPOAbuse.py {' '} - can be used for that purpose. + can also be used for this purpose. This edge can be a false positive in rare scenarios. If you have GenericWrite on the GPO with @@ -212,46 +251,62 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - With GenericWrite permissions over an OU, you may make modifications to the gPLink attribute of - the OU. The ability to alter the gPLink attribute of an OU may allow an attacker to apply a - malicious Group Policy Object (GPO) to all of the OU's child user and computer objects - (including the ones located in nested sub-OUs). This can be exploited to make said child items - execute arbitrary commands through an immediate scheduled task, thus compromising them. + GenericWrite permissions on an OU allow you to modify its gPLink attribute. This can be abused + to link a malicious Group Policy Object (GPO) to the OU, applying it to the OU's users and + computers, including those in nested OUs. The linked GPO can force those child objects to + execute arbitrary commands, for example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + From Linux, you can use the{' '} OUned.py {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + tool to exploit this gPLink manipulation path. For requirements and implementation details, see{' '} - the article associated to the OUned.py tool + the accompanying OUned.py article . - - Be mindful of the number of users and computers that are in the given OU as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target OU through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the OU with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "OU=SERVERS,DC=corp,DC=com" -n "TARGETGPO"' + } - Alternatively, the ability to modify the gPLink attribute of an OU can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target OU through - its gPLink attribute, making all child users and computers apply the malicious GPO and execute - arbitrary commands. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -259,46 +314,62 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - With GenericWrite permission over a domain object, you may make modifications to the gPLink - attribute of the domain. The ability to alter the gPLink attribute of a domain may allow an - attacker to apply a malicious Group Policy Object (GPO) to all of the domain user and computer - objects (including the ones located in nested OUs). This can be exploited to make said child - items execute arbitrary commands through an immediate scheduled task, thus compromising them. + GenericWrite on a domain object allows you to modify its gPLink attribute. This can be abused to + link a malicious Group Policy Object (GPO) to the domain, applying it to the domain's users and + computers, including those in nested OUs. The linked GPO can force those child objects to + execute arbitrary commands, for example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + From Linux, you can use the{' '} OUned.py {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + tool to exploit this gPLink manipulation path. For requirements and implementation details, see{' '} - the article associated to the OUned.py tool + the accompanying OUned.py article . - - Be mindful of the number of users and computers that are in the given domain as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target domain object through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the domain with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "DC=corp,DC=com" -n "TARGETGPO"' + } - Alternatively, the ability to modify the gPLink attribute of a domain can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target domain - through its gPLink attribute, making all child users and computers apply the malicious GPO and - execute arbitrary commands. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -354,6 +425,71 @@ const LinuxAbuse: FC = ({ targetType }) => { ); + case 'Site': + return ( + <> + + GenericWrite permissions on a site object allow you to modify its gPLink attribute. A malicious + Group Policy Object (GPO) linked to the site can force affected computers and users to execute + arbitrary commands, for example through an immediate scheduled task.{' '} + + + For site objects, affected computers include the site's domain controllers, and also computers + whose IP addresses fall within one of the site's subnets. If the site is the default site, + affected computers also include computers that do not map to any other site. Affected users are + those who sign in to the affected computers. + + + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From Linux, see{' '} + + the Site Unseen article + {' '} + for site-specific gPLink attack requirements and implementation details. + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target site object through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the site object with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=corp,DC=com" -n "TARGETGPO"' + } + + + + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); default: return ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/References.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/References.tsx index 65e7b9c24a42..7b0250510bc3 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/References.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/References.tsx @@ -123,6 +123,13 @@ const References: FC = () => { https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53
+ + https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites + +
= ({ sourceName, sourceType, targetName, t return ( <> - With GenericWrite on a GPO, you may make modifications to that GPO which will then apply to the - users and computers affected by the GPO. Select the target object you wish to push an evil - policy down to, then use the gpedit GUI to modify the GPO, using an evil policy that allows - item-level targeting, such as a new immediate scheduled task. Then wait for the group policy - client to pick up and execute the new evil policy. See the references tab for a more detailed - write up on this abuse. + GenericWrite on a GPO allows you to modify that GPO and inject malicious configuration. For + example, you can add an immediate scheduled task that runs on the computers or users that + process the GPO, compromising those objects. Some settings, including scheduled tasks, support + item-level targeting, which can limit execution to specific objects. GPOs apply every 90 minutes + for standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain + controllers. See the References tab for more detail. + + + + On a domain-joined Windows machine, you can edit GPOs with the native Group Policy Management + Console (GPMC). On a non-domain-joined Windows machine, use the{' '} + + DRSAT (Disconnected RSAT) + {' '} + tool. This edge can be a false positive in rare scenarios. If you have GenericWrite on the GPO with @@ -268,24 +277,23 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t return ( <> - With GenericWrite permissions over an OU, you may make modifications to the gPLink attribute of - the OU. The ability to alter the gPLink attribute of an OU may allow an attacker to apply a - malicious Group Policy Object (GPO) to all of the OU's child user and computer objects - (including the ones located in nested sub-OUs). This can be exploited to make said child items - execute arbitrary commands through an immediate scheduled task, thus compromising them. + GenericWrite permissions on an OU allow you to modify its gPLink attribute. This can be abused + to link a malicious Group Policy Object (GPO) to the OU, applying it to the OU's users and + computers, including those in nested OUs. The linked GPO can force those child objects to + execute arbitrary commands, for example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a domain-joined compromised Windows machine, the gPLink manipulation attack vector may be - exploited through Powermad, PowerView and native Windows functionalities. For a detailed outline - of exploit requirements and implementation, you can refer to{' '} + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For requirements and implementation + details, see{' '} = ({ sourceName, sourceType, targetName, t - Be mindful of the number of users and computers that are in the given OU as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target OU through its gPLink attribute. - Alternatively, the ability to modify the gPLink attribute of an OU can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target OU through - its gPLink attribute, making all child users and computers apply the malicious GPO and execute - arbitrary commands. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -313,24 +319,23 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t return ( <> - With GenericWrite permission over a domain object, you may make modifications to the gPLink - attribute of the domain. The ability to alter the gPLink attribute of a domain may allow an - attacker to apply a malicious Group Policy Object (GPO) to all of the domain user and computer - objects (including the ones located in nested OUs). This can be exploited to make said child - items execute arbitrary commands through an immediate scheduled task, thus compromising them. + GenericWrite on a domain object allows you to modify its gPLink attribute. This can be abused to + link a malicious Group Policy Object (GPO) to the domain, applying it to the domain's users and + computers, including those in nested OUs. The linked GPO can force those child objects to + execute arbitrary commands, for example through an immediate scheduled task. - Successful exploitation will require the possibility to add non-existing DNS records to the - domain and to create machine accounts. Alternatively, an already compromised domain-joined - machine may be used to perform the attack. Note that the attack vector implementation is not - trivial and will require some setup. + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. - From a domain-joined compromised Windows machine, the gPLink manipulation attack vector may be - exploited through Powermad, PowerView and native Windows functionalities. For a detailed outline - of exploit requirements and implementation, you can refer to{' '} + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For requirements and implementation + details, see{' '} = ({ sourceName, sourceType, targetName, t - Be mindful of the number of users and computers that are in the given domain as they all will - attempt to fetch and apply the malicious GPO. + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target domain object through its gPLink attribute. - Alternatively, the ability to modify the gPLink attribute of a domain can be exploited in - conjunction with write permissions on a GPO. In such a situation, an attacker could first inject - a malicious scheduled task in the controlled GPO, and then link the GPO to the target domain - through its gPLink attribute, making all child users and computers apply the malicious GPO and - execute arbitrary commands. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -406,6 +409,53 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t ); + case 'Site': + return ( + <> + + GenericWrite permissions on a site object allow you to modify its gPLink attribute. A malicious + Group Policy Object (GPO) linked to the site can force affected computers and users to execute + arbitrary commands, for example through an immediate scheduled task.{' '} + + + For site objects, affected computers include the site's domain controllers, and also computers + whose IP addresses fall within one of the site's subnets. If the site is the default site, + affected computers also include computers that do not map to any other site. Affected users are + those who sign in to the affected computers. + + + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For site-specific requirements and + implementation details, see{' '} + + the Site Unseen article + + . + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target site object through its gPLink attribute. + + + + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); default: return ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/General.tsx index 9b031a4cb268..a26df168efc8 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/General.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/General.tsx @@ -23,29 +23,25 @@ const General: FC = ({ sourceName, sourceType, targetType, target return ( <> - {groupSpecialFormat(sourceType, sourceName)} has the permissions to modify the gPLink attribute of{' '} + {groupSpecialFormat(sourceType, sourceName)} has permission to modify the gPLink attribute of{' '} {targetType} {targetName}. - The ability to alter the gPLink attribute may allow an attacker to apply a malicious Group Policy Object - (GPO) to all child user and computer objects (including the ones located in nested OUs). This can be - exploited to make said child objects execute arbitrary commands through an immediate scheduled task, - thus compromising them. + Modifying an object's gPLink attribute can allow an attacker to link a malicious Group Policy Object + (GPO) so it applies to affected users and computers. That GPO can force those objects to execute + arbitrary commands, for example through an immediate scheduled task. - - Successful exploitation will require the possibility to add non-existing DNS records to the domain and - to create machine accounts. Alternatively, an already compromised domain-joined machine may be used to - perform the attack. Note that the attack vector implementation is not trivial and will require some - setup. + For domain and OU objects, affected child users and computers include those contained directly within + the domain or OU, as well as those in nested OUs. However, unless the GPO link is enforced, some users + and computers may not be affected if GPO inheritance is blocked on a containing OU. - - Alternatively, the ability to modify the gPLink attribute can be exploited in conjunction with write - permissions on a GPO. In such a situation, an attacker could first inject a malicious scheduled task in - the controlled GPO, and then link the GPO to the target through its gPLink attribute, making all child - users and computers apply the malicious GPO and execute arbitrary commands. + For site objects, affected computers include the site's domain controllers, and also computers whose IP + addresses fall within one of the site's subnets. If the site is the default site, affected computers + also include computers that do not map to any other site. Affected users are those who sign in to the + affected computers. ); diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/LinuxAbuse.tsx index 666d9a73aa05..1094f2d0e89d 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/LinuxAbuse.tsx @@ -17,31 +17,182 @@ import { Link } from '@mui/material'; import { Typography } from 'doodle-ui'; import { FC } from 'react'; +import { EdgeInfoProps } from '../index'; -const LinuxAbuse: FC = () => { - return ( - <> - - From a Linux machine, the WriteGPLink permission may be abused using the{' '} - - OUned.py - {' '} - exploitation tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} - - the article associated to the OUned.py tool - - . - - - - Be mindful of the number of users and computers that are in the given domain as they all will attempt to - fetch and apply the malicious GPO. - - - ); +const LinuxAbuse: FC = ({ targetType }) => { + switch (targetType) { + case 'Domain': + return ( + <> + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From Linux, you can use the{' '} + + OUned.py + {' '} + tool to exploit this gPLink manipulation path. For requirements and implementation details, see{' '} + + the accompanying OUned.py article + + . + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target domain object through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the domain with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "DC=corp,DC=com" -n "TARGETGPO"' + } + + + + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. + + + ); + case 'OU': + return ( + <> + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From Linux, you can use the{' '} + + OUned.py + {' '} + tool to exploit this gPLink manipulation path. For requirements and implementation details, see{' '} + + the accompanying OUned.py article + + . + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target OU through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the OU with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "OU=SERVERS,DC=corp,DC=com" -n "TARGETGPO"' + } + + + + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); + case 'Site': + return ( + <> + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From Linux, see{' '} + + the Site Unseen article + {' '} + for site-specific gPLink attack requirements and implementation details. + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target site object through its gPLink attribute. You can use{' '} + + GroupPolicyBackdoor.py + {' '} + for this. For example, first inject the malicious configuration with the 'inject' command. + + + { + 'python3 gpb.py gpo inject -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -m Scheduled_task_add.ini -n "TARGETGPO"' + } + + + Then link the modified GPO to the site object with the 'link' command. + + + { + 'python3 gpb.py links link -d "corp.com" --dc "dc.corp.com" -u "user" -p "password" -o "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=corp,DC=com" -n "TARGETGPO"' + } + + + + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); + default: + return ( + <> + No abuse information available for this node type. + + ); + } }; export default LinuxAbuse; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/References.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/References.tsx index 8e8dc3490b92..1af89a6b799d 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/References.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/References.tsx @@ -33,6 +33,13 @@ const References: FC = () => { href='https://www.synacktiv.com/publications/ounedpy-exploiting-hidden-organizational-units-acl-attack-vectors-in-active-directory'> https://www.synacktiv.com/publications/ounedpy-exploiting-hidden-organizational-units-acl-attack-vectors-in-active-directory +
+ + https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites + ); }; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/WindowsAbuse.tsx index 040b52f6fba4..ad39f53e91d7 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/WindowsAbuse.tsx @@ -17,29 +17,122 @@ import { Link } from '@mui/material'; import { Typography } from 'doodle-ui'; import { FC } from 'react'; +import { EdgeInfoProps } from '../index'; -const WindowsAbuse: FC = () => { - return ( - <> - - From a domain-joined compromised Windows machine, the WriteGPLink permission may be abused through - Powermad, PowerView and native Windows functionalities. For a detailed outline of exploit requirements - and implementation, you can refer to{' '} - - this article - - . - - - - Be mindful of the number of users and computers that are in the given domain as they all will attempt to - fetch and apply the malicious GPO. - - - ); +const WindowsAbuse: FC = ({ targetType }) => { + switch (targetType) { + case 'Domain': + return ( + <> + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For requirements and implementation + details, see{' '} + + this article + + . + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target domain object through its gPLink attribute. + + + + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. + + + ); + case 'OU': + return ( + <> + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For requirements and implementation + details, see{' '} + + this article + + . + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target OU through its gPLink attribute. + + + + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); + case 'Site': + return ( + <> + + If you do not control an existing GPO and cannot create one, exploitation requires the ability + to create machine accounts and add DNS records that do not already exist in the domain. An + already compromised domain-joined machine can also be used. Executing this attack vector is not + trivial and requires setup. + + + + From a compromised domain-joined Windows machine, you can exploit this gPLink manipulation path + with Powermad, PowerView, and native Windows functionality. For site-specific requirements and + implementation details, see{' '} + + the Site Unseen article + + . + + + + If you control an existing GPO or can create one, the attack is simpler: inject a malicious + configuration, such as an immediate scheduled task, into a controlled GPO, then link that GPO to + the target site object through its gPLink attribute. + + + + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. + + + ); + default: + return ( + <> + No abuse information available for this node type. + + ); + } }; export default WindowsAbuse; diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 8c3b91886467..00ddd59471d3 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -30,6 +30,9 @@ export enum ActiveDirectoryNodeKind { NTAuthStore = 'NTAuthStore', CertTemplate = 'CertTemplate', IssuancePolicy = 'IssuancePolicy', + Site = 'Site', + SiteServer = 'SiteServer', + SiteSubnet = 'SiteSubnet', } export function ActiveDirectoryNodeKindToDisplay(value: ActiveDirectoryNodeKind): string | undefined { switch (value) { @@ -65,6 +68,12 @@ export function ActiveDirectoryNodeKindToDisplay(value: ActiveDirectoryNodeKind) return 'CertTemplate'; case ActiveDirectoryNodeKind.IssuancePolicy: return 'IssuancePolicy'; + case ActiveDirectoryNodeKind.Site: + return 'Site'; + case ActiveDirectoryNodeKind.SiteServer: + return 'SiteServer'; + case ActiveDirectoryNodeKind.SiteSubnet: + return 'SiteSubnet'; default: return undefined; } @@ -81,6 +90,7 @@ export enum ActiveDirectoryRelationshipKind { AddMember = 'AddMember', HasSession = 'HasSession', Contains = 'Contains', + ServerIs = 'ServerIs', GPLink = 'GPLink', AllowedToDelegate = 'AllowedToDelegate', CoerceToTGT = 'CoerceToTGT', @@ -183,6 +193,8 @@ export function ActiveDirectoryRelationshipKindToDisplay(value: ActiveDirectoryR return 'HasSession'; case ActiveDirectoryRelationshipKind.Contains: return 'Contains'; + case ActiveDirectoryRelationshipKind.ServerIs: + return 'ServerIs'; case ActiveDirectoryRelationshipKind.GPLink: return 'GPLink'; case ActiveDirectoryRelationshipKind.AllowedToDelegate: @@ -500,6 +512,7 @@ export enum ActiveDirectoryKindProperties { NetBIOS = 'netbios', AdminSDHolderProtected = 'adminsdholderprotected', ServicePrincipalNames = 'serviceprincipalnames', + Serverreference = 'serverreference', GPOStatusRaw = 'gpostatusraw', GPOStatus = 'gpostatus', } @@ -781,6 +794,8 @@ export function ActiveDirectoryKindPropertiesToDisplay(value: ActiveDirectoryKin return 'AdminSDHolder Protected'; case ActiveDirectoryKindProperties.ServicePrincipalNames: return 'Service Principal Names'; + case ActiveDirectoryKindProperties.Serverreference: + return 'Server Reference'; case ActiveDirectoryKindProperties.GPOStatusRaw: return 'GPO Status (Raw)'; case ActiveDirectoryKindProperties.GPOStatus: @@ -850,6 +865,7 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi ActiveDirectoryRelationshipKind.WritePublicInformation, ActiveDirectoryRelationshipKind.ManageCA, ActiveDirectoryRelationshipKind.ManageCertificates, + ActiveDirectoryRelationshipKind.ServerIs, ActiveDirectoryRelationshipKind.Contains, ActiveDirectoryRelationshipKind.DCFor, ActiveDirectoryRelationshipKind.SameForestTrust, @@ -914,6 +930,7 @@ export function ActiveDirectoryPathfindingEdgesMatchFrontend(): ActiveDirectoryR ActiveDirectoryRelationshipKind.WritePublicInformation, ActiveDirectoryRelationshipKind.ManageCA, ActiveDirectoryRelationshipKind.ManageCertificates, + ActiveDirectoryRelationshipKind.ServerIs, ActiveDirectoryRelationshipKind.Contains, ActiveDirectoryRelationshipKind.DCFor, ActiveDirectoryRelationshipKind.SameForestTrust, diff --git a/packages/javascript/bh-shared-ui/src/utils/content.ts b/packages/javascript/bh-shared-ui/src/utils/content.ts index 1431e683eb45..223d88626cb8 100644 --- a/packages/javascript/bh-shared-ui/src/utils/content.ts +++ b/packages/javascript/bh-shared-ui/src/utils/content.ts @@ -173,6 +173,11 @@ export const entityInformationEndpoints: Record apiClient.getUserV2(id, false, options), [ActiveDirectoryNodeKind.IssuancePolicy]: (id: string, options?: RequestOptions) => apiClient.getIssuancePolicyV2(id, false, options), + [ActiveDirectoryNodeKind.Site]: (id: string, options?: RequestOptions) => apiClient.getSiteV2(id, false, options), + [ActiveDirectoryNodeKind.SiteServer]: (id: string, options?: RequestOptions) => + apiClient.getSiteServerV2(id, false, options), + [ActiveDirectoryNodeKind.SiteSubnet]: (id: string, options?: RequestOptions) => + apiClient.getSiteSubnetV2(id, false, options), }; export const allSections: Partial EntityInfoDataTableProps[]>> = { @@ -922,6 +927,11 @@ export const allSections: Partial EntityInfo label: 'Users', queryType: 'gpo-users', }, + { + id, + label: 'Sites', + queryType: 'gpo-sites', + }, { id, label: 'Tier Zero Objects', @@ -1046,6 +1056,30 @@ export const allSections: Partial EntityInfo queryType: 'issuancepolicy-linked_certificate_templates', }, ], + [ActiveDirectoryNodeKind.Site]: (id: string) => [ + { + id, + label: 'Inbound Object Control', + queryType: 'site-inbound_object_control', + }, + { + id, + label: 'Linked GPOs', + queryType: 'site-linked_gpos', + }, + { + id, + label: 'Linked Site Servers', + queryType: 'site-linked_siteservers', + }, + { + id, + label: 'Linked Site Subnets', + queryType: 'site-linked_sitesubnets', + }, + ], + [ActiveDirectoryNodeKind.SiteServer]: () => [], + [ActiveDirectoryNodeKind.SiteSubnet]: () => [], [ActiveDirectoryNodeKind.User]: (id: string) => [ { id, @@ -1783,6 +1817,8 @@ export const entityRelationshipEndpoints = { apiClient.getGPOComputersV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), 'gpo-users': ({ id, skip, limit, type }) => apiClient.getGPOUsersV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), + 'gpo-sites': ({ id, skip, limit, type }) => + apiClient.getGPOSitesV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), 'gpo-tier_zero_objects': ({ id, skip, limit, type }) => apiClient.getGPOTierZeroV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), 'gpo-inbound_object_control': ({ id, skip, limit, type }) => @@ -1835,6 +1871,14 @@ export const entityRelationshipEndpoints = { apiClient .getIssuancePolicyLinkedTemplatesV2(id, skip, limit, type, { signal: controller.signal }) .then((res) => res.data), + 'site-inbound_object_control': ({ id, skip, limit, type }) => + apiClient.getSiteControllersV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), + 'site-linked_gpos': ({ id, skip, limit, type }) => + apiClient.getSiteLinkedGPOsV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), + 'site-linked_siteservers': ({ id, skip, limit, type }) => + apiClient.getSiteLinkedServersV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), + 'site-linked_sitesubnets': ({ id, skip, limit, type }) => + apiClient.getSiteLinkedSubnetsV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), 'user-sessions': ({ id, skip, limit, type }) => apiClient.getUserSessionsV2(id, skip, limit, type, { signal: controller.signal }).then((res) => res.data), 'user-member_of': ({ id, skip, limit, type }) => diff --git a/packages/javascript/bh-shared-ui/src/utils/icons.ts b/packages/javascript/bh-shared-ui/src/utils/icons.ts index d50d03f05b58..4e2ac6eb427c 100644 --- a/packages/javascript/bh-shared-ui/src/utils/icons.ts +++ b/packages/javascript/bh-shared-ui/src/utils/icons.ts @@ -34,6 +34,9 @@ import { faLandmark, faList, faLock, + faMap, + faMapMarker, + faMapSigns, faObjectGroup, faQuestion, faRobot, @@ -125,6 +128,21 @@ export const NODE_ICONS: IconDictionary = { color: '#99B2DD', }, + [ActiveDirectoryNodeKind.Site]: { + icon: faMapSigns, + color: '#2DD4BF', + }, + + [ActiveDirectoryNodeKind.SiteServer]: { + icon: faMapMarker, + color: '#60A5FA', + }, + + [ActiveDirectoryNodeKind.SiteSubnet]: { + icon: faMap, + color: '#F59E0B', + }, + [ActiveDirectoryNodeKind.OU]: { icon: faSitemap, color: '#FFAA00', diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx b/packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx index 87ce459f634a..c8b78a5a9b68 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx @@ -54,6 +54,9 @@ export const DomainMap = { ntauthstores: { displayText: 'NTAuthStores', kind: ActiveDirectoryNodeKind.NTAuthStore }, certtemplates: { displayText: 'CertTemplates', kind: ActiveDirectoryNodeKind.CertTemplate }, issuancepolicies: { displayText: 'IssuancePolicies', kind: ActiveDirectoryNodeKind.IssuancePolicy }, + sites: { displayText: 'Sites', kind: ActiveDirectoryNodeKind.Site }, + siteservers: { displayText: 'SiteServers', kind: ActiveDirectoryNodeKind.SiteServer }, + sitesubnets: { displayText: 'SiteSubnets', kind: ActiveDirectoryNodeKind.SiteSubnet }, containers: { displayText: 'Containers', kind: ActiveDirectoryNodeKind.Container, diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/BasicObjectInfoFields.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/BasicObjectInfoFields.tsx index 78abc7c70738..7ab1a126a3c3 100644 --- a/packages/javascript/bh-shared-ui/src/views/Explore/BasicObjectInfoFields.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/BasicObjectInfoFields.tsx @@ -32,7 +32,11 @@ interface BasicObjectInfoFieldsProps { noderesourcegroupid?: string; nodeType?: string; objectid?: string; + serverreferencecomputer?: string; + serverreferencecomputername?: string; service_principal_id?: string; + siteservernode?: string; + siteservernodename?: string; federatedidentitycredentialappid?: string; } @@ -41,8 +45,11 @@ const RelatedKindField = ( fieldLabel: string, relatedKind: EntityKinds, id: string, - name?: string + name?: string, + displayValue?: string ) => { + const value = displayValue || id; + return ( @@ -52,12 +59,14 @@ const RelatedKindField = ( onSourceNodeSelected({ objectid: id, type: relatedKind, name: name || '' })} + onClick={() => + onSourceNodeSelected({ objectid: id, type: relatedKind, name: name || displayValue || '' }) + } style={{ cursor: 'pointer' }} overflow='hidden' textOverflow='ellipsis' - title={id}> - {id} + title={value}> + {value} @@ -92,6 +101,24 @@ export const BasicObjectInfoFields: React.FC = (prop props.service_principal_id, props.name )} + {props.serverreferencecomputer && + RelatedKindField( + props.handleSourceNodeSelected, + 'Referenced Computer:', + ActiveDirectoryNodeKind.Computer, + props.serverreferencecomputer, + props.serverreferencecomputername, + props.serverreferencecomputername + )} + {props.siteservernode && + RelatedKindField( + props.handleSourceNodeSelected, + 'Site Server:', + ActiveDirectoryNodeKind.SiteServer, + props.siteservernode, + props.siteservernodename, + props.siteservernodename + )} {props.federatedidentitycredentialappid && RelatedKindField( props.handleSourceNodeSelected, diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/EdgeFilter/edgeCategories.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/EdgeFilter/edgeCategories.tsx index 3609e28ac5d7..afcb98866e72 100644 --- a/packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/EdgeFilter/edgeCategories.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/EdgeFilter/edgeCategories.tsx @@ -51,6 +51,7 @@ export const BUILTIN_EDGE_CATEGORIES: Category[] = [ ActiveDirectoryRelationshipKind.HasSIDHistory, ActiveDirectoryRelationshipKind.MemberOf, ActiveDirectoryRelationshipKind.SameForestTrust, + ActiveDirectoryRelationshipKind.ServerIs, ], }, { diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx index 743aa014e7b4..7a73bd837bc5 100644 --- a/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx @@ -36,6 +36,10 @@ export const exclusionList = [ CommonKindProperties.DisplayName, AzureKindProperties.ServicePrincipalID, AzureKindProperties.FederatedIdentityCredentialAppID, + 'serverreferencecomputer', + 'serverreferencecomputername', + 'siteservernode', + 'siteservernodename', 'highvalue', 'reconcile', ActiveDirectoryKindProperties.InheritanceHashes, diff --git a/packages/javascript/js-client-library/src/client.ts b/packages/javascript/js-client-library/src/client.ts index dfdd67d4546c..ddad0a8fec61 100644 --- a/packages/javascript/js-client-library/src/client.ts +++ b/packages/javascript/js-client-library/src/client.ts @@ -1857,6 +1857,21 @@ class BHEAPIClient { ) ); + getGPOSitesV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/gpos/${id}/sites`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + getGPOControllersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => this.baseClient.get( `/api/v2/gpos/${id}/controllers`, @@ -2626,6 +2641,102 @@ class BHEAPIClient { ) ); + getSiteV2 = (id: string, counts?: boolean, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sites/${id}`, + Object.assign( + { + params: { + counts, + }, + }, + options + ) + ); + + getSiteControllersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sites/${id}/controllers`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + getSiteLinkedGPOsV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sites/${id}/linked-gpos`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + getSiteLinkedServersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sites/${id}/siteservers`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + getSiteLinkedSubnetsV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sites/${id}/sitesubnets`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + + getSiteServerV2 = (id: string, counts?: boolean, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/siteservers/${id}`, + Object.assign( + { + params: { + counts, + }, + }, + options + ) + ); + + getSiteSubnetV2 = (id: string, counts?: boolean, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sitesubnets/${id}`, + Object.assign( + { + params: { + counts, + }, + }, + options + ) + ); + getMetaV2 = (id: string, options?: RequestOptions) => this.baseClient.get(`/api/v2/meta/${id}`, options); getShortestPathV2 = (startNode: string, endNode: string, relationshipKinds?: string, options?: RequestOptions) =>