From 6fe1f933c6100c11e0caebe0030e8dbd50605b06 Mon Sep 17 00:00:00 2001 From: q-roland Date: Mon, 3 Nov 2025 19:20:40 +0100 Subject: [PATCH 01/15] feat: Add visualisation of Active Directory site data * Add Site, SiteSubnet and SiteServer nodes * Add GenericAll, GenericWrite and WriteGPLink edges on Site objects * Add gPLink edges from GPOs to site objects * Add Site objects to default high value targets --- cmd/api/src/analysis/ad/queries.go | 12 + .../api/bloodhoundgraph/bloodhoundgraph.go | 12 + cmd/api/src/api/registration/v2.go | 15 ++ cmd/api/src/api/v2/ad_entity.go | 31 +++ cmd/api/src/api/v2/ad_related_entity.go | 12 + .../database/migration/migrations/v8.4.0.sql | 55 +++++ cmd/api/src/model/adquality.go | 6 + cmd/api/src/model/ingest/ingest.go | 14 +- cmd/api/src/services/graphify/convertors.go | 34 +++ cmd/api/src/services/graphify/ingest.go | 3 + cmd/ui/src/ducks/graph/graphutils.ts | 3 + cmd/ui/src/ducks/graph/types.ts | 3 + packages/cue/bh/ad/ad.cue | 18 ++ packages/go/analysis/ad/filters.go | 4 + packages/go/analysis/ad/queries.go | 70 +++++- packages/go/ein/incoming_models.go | 9 + packages/go/graphschema/ad/ad.go | 7 +- .../HelpTexts/GPLink/LinuxAbuse.tsx | 56 ++++- .../HelpTexts/GPLink/WindowsAbuse.tsx | 24 +- .../HelpTexts/GenericAll/LinuxAbuse.tsx | 222 ++++++++++++++---- .../HelpTexts/GenericAll/References.tsx | 6 + .../HelpTexts/GenericAll/WindowsAbuse.tsx | 138 +++++++---- .../HelpTexts/GenericWrite/LinuxAbuse.tsx | 192 ++++++++++++--- .../HelpTexts/GenericWrite/References.tsx | 6 + .../HelpTexts/GenericWrite/WindowsAbuse.tsx | 126 +++++++--- .../HelpTexts/WriteGPLink/General.tsx | 22 +- .../HelpTexts/WriteGPLink/LinuxAbuse.tsx | 192 +++++++++++++-- .../HelpTexts/WriteGPLink/References.tsx | 6 + .../HelpTexts/WriteGPLink/WindowsAbuse.tsx | 135 +++++++++-- .../bh-shared-ui/src/graphSchema.ts | 9 + .../bh-shared-ui/src/utils/content.ts | 57 +++++ .../bh-shared-ui/src/utils/icons.ts | 18 ++ .../src/views/DataQuality/DomainInfo.tsx | 3 + .../js-client-library/src/client.ts | 127 ++++++++++ 34 files changed, 1396 insertions(+), 251 deletions(-) diff --git a/cmd/api/src/analysis/ad/queries.go b/cmd/api/src/analysis/ad/queries.go index 372a8157261f..9fdcff056ce3 100644 --- a/cmd/api/src/analysis/ad/queries.go +++ b/cmd/api/src/analysis/ad/queries.go @@ -170,6 +170,18 @@ func GraphStats(ctx context.Context, db graph.Database) (model.ADDataQualityStat 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/api/bloodhoundgraph/bloodhoundgraph.go b/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go index b86bf5f74bd6..4891b8a62805 100644 --- a/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go +++ b/cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go @@ -258,6 +258,18 @@ func (s *BloodHoundGraphNode) SetIcon(nType string) { s.FontIcon = &BloodHoundGraphFontIcon{ Text: "fas fa-clipboard-check", } + case "Site": + s.FontIcon = &BloodHoundGraphFontIcon{ + Text: "fas fa-map-signs", + } + case "SiteServer": + s.FontIcon = &BloodHoundGraphFontIcon{ + Text: "fas fa-map-marker", + } + case "SiteSubnet": + s.FontIcon = &BloodHoundGraphFontIcon{ + Text: "fas fa-map", + } case "Meta": if tier, ok := s.Data["admintier"]; ok { if tier.(int64) == 0 { diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index 5e1d196e48e1..829ae8306bd1 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -276,6 +276,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), @@ -343,6 +344,20 @@ 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}/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), + routerInst.GET(fmt.Sprintf("/api/v2/siteservers/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead), + + // SiteSubnet Entity API + routerInst.GET(fmt.Sprintf("/api/v2/sitesubnets/{%s}", api.URIPathVariableObjectID), resources.GetSiteSubnetEntityInfo).RequirePermissions(permissions.GraphDBRead), + routerInst.GET(fmt.Sprintf("/api/v2/sitesubnets/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).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), routerInst.GET(fmt.Sprintf("/api/v2/azure-tenants/{%s}/data-quality-stats", api.URIPathVariableTenantID), resources.GetAzureDataQualityStats).RequirePermissions(permissions.GraphDBRead), diff --git a/cmd/api/src/api/v2/ad_entity.go b/cmd/api/src/api/v2/ad_entity.go index 6e70401a0f5b..2b21a7088abe 100644 --- a/cmd/api/src/api/v2/ad_entity.go +++ b/cmd/api/src/api/v2/ad_entity.go @@ -160,6 +160,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), } @@ -282,3 +283,33 @@ 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, + } + ) + + s.handleAdEntityInfoQuery(response, request, ad.Site, countQueries) +} + +func (s *Resources) GetSiteServerEntityInfo(response http.ResponseWriter, request *http.Request) { + var ( + countQueries = map[string]any{ + "controllers": adAnalysis.FetchInboundADEntityControllers, + } + ) + + s.handleAdEntityInfoQuery(response, request, ad.SiteServer, countQueries) +} + +func (s *Resources) GetSiteSubnetEntityInfo(response http.ResponseWriter, request *http.Request) { + var ( + countQueries = map[string]any{ + "controllers": adAnalysis.FetchInboundADEntityControllers, + } + ) + + s.handleAdEntityInfoQuery(response, request, ad.SiteSubnet, countQueries) +} diff --git a/cmd/api/src/api/v2/ad_related_entity.go b/cmd/api/src/api/v2/ad_related_entity.go index 55cc4533d521..82fd43cfe5ba 100644 --- a/cmd/api/src/api/v2/ad_related_entity.go +++ b/cmd/api/src/api/v2/ad_related_entity.go @@ -205,6 +205,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.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectSitesCandidateFilter)) +} + func (s *Resources) ListADGPOAffectedComputers(response http.ResponseWriter, request *http.Request) { s.handleAdRelatedEntityQuery(response, request, "ListADGPOAffectedComputers", adAnalysis.CreateGPOAffectedIntermediariesPathDelegate(ad.Computer), adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectComputersCandidateFilter)) } @@ -240,3 +244,11 @@ 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) 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/database/migration/migrations/v8.4.0.sql b/cmd/api/src/database/migration/migrations/v8.4.0.sql index cbdbc4dd2652..635a6e3257e1 100644 --- a/cmd/api/src/database/migration/migrations/v8.4.0.sql +++ b/cmd/api/src/database/migration/migrations/v8.4.0.sql @@ -42,3 +42,58 @@ JOIN permissions p )) ) ON CONFLICT DO NOTHING; + +-- Adding site specific columns to ad_data_quality_aggregations and ad_data_quality_stats tables +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; + + +-- Add Sites default selector to Tier Zero +WITH src_data AS ( + SELECT * FROM (VALUES +-- START +('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.') +-- END + ) 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, + 'SYSTEM', + current_timestamp, + 'SYSTEM', + CASE WHEN NOT d.enabled THEN current_timestamp ELSE NULL END, + CASE WHEN NOT d.enabled THEN 'SYSTEM' 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; 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 345e99381766..22b13feeddb9 100644 --- a/cmd/api/src/model/ingest/ingest.go +++ b/cmd/api/src/model/ingest/ingest.go @@ -78,8 +78,14 @@ func (s Metadata) MatchKind() (graph.Kind, bool) { return ad.CertTemplate, true case DataTypeIssuancePolicy: return ad.IssuancePolicy, true - } + case DataTypeSite: + return ad.Site, true + case DataTypeSiteServer: + return ad.SiteServer, true + case DataTypeSiteSubnet: + return ad.SiteSubnet, true + } return nil, false } @@ -103,6 +109,9 @@ const ( DataTypeCertTemplate DataType = "certtemplates" DataTypeAzure DataType = "azure" DataTypeIssuancePolicy DataType = "issuancepolicies" + DataTypeSite DataType = "sites" + DataTypeSiteServer DataType = "siteservers" + DataTypeSiteSubnet DataType = "sitesubnets" DataTypeOpenGraph DataType = "opengraph" ) @@ -125,6 +134,9 @@ func AllIngestDataTypes() []DataType { DataTypeCertTemplate, DataTypeAzure, DataTypeIssuancePolicy, + DataTypeSite, + DataTypeSiteServer, + DataTypeSiteSubnet, } } diff --git a/cmd/api/src/services/graphify/convertors.go b/cmd/api/src/services/graphify/convertors.go index 1acab6af23ab..35b211f47cf9 100644 --- a/cmd/api/src/services/graphify/convertors.go +++ b/cmd/api/src/services/graphify/convertors.go @@ -299,3 +299,37 @@ 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.ConvertObjectToNode(site.IngestBase, ad.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.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(ein.IngestBase(siteServer), ad.SiteServer, ingestTime) + converted.NodeProps = append(converted.NodeProps, baseNodeProp) + converted.RelProps = append(converted.RelProps, ein.ParseACEData(baseNodeProp, siteServer.Aces, siteServer.ObjectIdentifier, ad.SiteServer)...) + + if rel := ein.ParseObjectContainer(ein.IngestBase(siteServer), 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) + converted.RelProps = append(converted.RelProps, ein.ParseACEData(baseNodeProp, siteSubnet.Aces, siteSubnet.ObjectIdentifier, ad.SiteSubnet)...) + + 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 2d95c63cdd95..e0cb14dda5cf 100644 --- a/cmd/api/src/services/graphify/ingest.go +++ b/cmd/api/src/services/graphify/ingest.go @@ -339,6 +339,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 a408d92aeb3a..98bf024c359d 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-clipboard-check', + [GraphNodeTypes.SiteServer]: 'fa-clipboard-check', + [GraphNodeTypes.SiteSubnet]: 'fa-clipboard-check', }; 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/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index d245a53c5654..d834cbc2e8dc 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -1261,6 +1261,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, @@ -1278,6 +1293,9 @@ NodeKinds: [ NTAuthStore, CertTemplate, IssuancePolicy, + Site, + SiteServer, + SiteSubnet ] Owns: types.#Kind & { 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 646b1e1153fb..1c05de59f0f1 100644 --- a/packages/go/analysis/ad/queries.go +++ b/packages/go/analysis/ad/queries.go @@ -210,7 +210,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 @@ -334,6 +334,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 @@ -373,6 +377,30 @@ 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 CreateGPOAffectedIntermediariesPathDelegate(targetKinds ...graph.Kind) analysis.PathDelegate { return func(tx graph.Transaction, node *graph.Node) (graph.PathSet, error) { pathSet := graph.NewPathSet() @@ -525,7 +553,7 @@ 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 !start.Kinds.ContainsOneOf(ad.OU, ad.Domain, ad.Site) { // If we run into anything that isn't an OU or a Domain node then we're done checking for // inheritance blocking return false @@ -583,7 +611,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), ) }, @@ -699,6 +727,42 @@ func CreateOUContainedPathDelegate(kind graph.Kind) analysis.PathDelegate { } } +func CreateSiteContainedListDelegate(kind graph.Kind) analysis.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) analysis.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) analysis.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/incoming_models.go b/packages/go/ein/incoming_models.go index 28e7d133c7b2..8c31fbae8122 100644 --- a/packages/go/ein/incoming_models.go +++ b/packages/go/ein/incoming_models.go @@ -174,6 +174,15 @@ type IssuancePolicy struct { GroupLink TypedPrincipal } +type Site struct { + IngestBase + Links []GPLink +} + +type SiteServer IngestBase + +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 4e21691829c9..2056efaf1883 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") @@ -1144,7 +1147,7 @@ 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, WriteOwnerRaw, OwnsLimitedRights, OwnsRaw, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, ProtectAdminGroups} @@ -1173,5 +1176,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/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GPLink/LinuxAbuse.tsx index bcbe1caa9a77..d87bd6915f17 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 @@ -22,19 +22,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 may make modifications to that GPO in order to inject malicious configurations into it. + You could for instance add a Scheduled Task that will then be executed by all of the computers and/or users to which the GPO applies, + thus compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing + to precisely target a specific object. + GPOs are applied every 90 minutes for standard objects (with a random offset of 0 to 30 minutes), and every 5 minutes for domain controllers. - - - pyGPOAbuse.py - {' '} - can be used for that purpose. - + + The + GroupPolicyBackdoor.py + {' '} + tool can be used to perform the attack from a Linux machine. First, define a module file that describes the configuration to inject. + The following one defines a computer configuration, with an immediate Scheduled Task adding a domain user as local administrator. + A filter is defined, so that it only applies 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"}]' + } + + + + Place the described configuration into the Scheduled_task_add.ini file, and 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. + ); }; 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 2935f9acfb2d..d6c12dcc23a0 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,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Typography } from '@mui/material'; +import { Link, Typography } from '@mui/material'; import { FC } from 'react'; import { EdgeInfoProps } from '../index'; @@ -22,11 +22,23 @@ 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 may make modifications to that GPO in order to inject malicious configurations into it. + You could for instance add a Scheduled Task that will then be executed by all of the computers and/or users to which the GPO applies, + thus compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing + to precisely target a specific object. + GPOs are applied 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 a more detailed write up on this abuse. + + + + On a domain-joined Windows machine, the native Group Policy Management Console (GPMC) may be used to edit GPOs. + On a non-domain joined Windows Machine, the{' '} + + DRSAT (Disconnected RSAT) + tool can be used. ); 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 99ac0c545e58..7c67f31e5667 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 @@ -394,20 +394,19 @@ 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 + In such a situation, it may still be possible to exploit GenericAll permissions on a domain + object. Indeed, with GenericAll permissions over a domain object, it is possible to modify its + gPLink attribute. This may be abused to apply a malicious Group Policy Object + (GPO) to all of the domain's user and computer objects (including the ones located in nested OUs). + This can be exploited to make said child objects execute arbitrary commands e.g. through an immediate scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -424,18 +423,31 @@ const LinuxAbuse: FC = ( . - - 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + You can then link the modified GPO to the domain, through 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. + 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. ); @@ -443,19 +455,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. + With full control over a GPO, you may make modifications to that GPO in order to inject malicious configurations into it. + You could for instance add a Scheduled Task that will then be executed by all of the computers and/or users to which the GPO applies, + thus compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing + to precisely target a specific object. + GPOs are applied 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 a more detailed write up on this abuse. - - pyGPOAbuse.py + The + GroupPolicyBackdoor.py {' '} - can be used for that purpose. + tool can be used to perform the attack from a Linux machine. First, define a module file that describes the configuration to inject. + The following one defines a computer configuration, with an immediate Scheduled Task adding a domain user as local administrator. + A filter is defined, so that it only applies 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"}]' + } + + + + Place the described configuration into the Scheduled_task_add.ini file, and 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. ); @@ -498,27 +544,26 @@ 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 such a situation, it may still be possible to exploit GenericAll permissions on an OU + object. Indeed, with GenericAll permissions over an OU, it is possible to modify its + gPLink attribute. This may be abused to apply a malicious Group Policy Object + (GPO) to all of the OU's user and computer objects (including the ones located in nested OUs). + This can be exploited to make said child objects execute arbitrary commands e.g. through an immediate + scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -535,18 +580,31 @@ const LinuxAbuse: FC = ( . - - 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target OU through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + You can then link the modified GPO to the OU, through 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. + 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. ); @@ -639,6 +697,70 @@ const LinuxAbuse: FC = ( ); + case 'Site': + return ( + <> + + GenericAll permissions over a Site object allow modifying the gPLink attribute of the site. + The ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious Group Policy Object + (GPO) to all of the objects associated with the site. This can be exploited to make said objects execute + arbitrary commands e.g. through an immediate scheduled task, thus compromising them. + In the case of a site, the affected objects are the computers that have an IP address included in one of the site's subnets + (or computers that do not belong to any site if this is the default site), as well as users connecting to these computers. + Note that Server objects associated with the Site should be located in the Site. + + + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + + OUned.py + {' '} + tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + + the article associated to the OUned.py tool + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) in that GPO, and then link the GPO to the target Site through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + Now you can link the modified GPO to the Site object, through 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"' + } + + + + Be mindful of the number of users and computers that are in the given site as they all will + attempt to fetch 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 af3477283806..c9915268c77e 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 @@ -207,6 +207,12 @@ 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 785594a90c7b..ab724f93896e 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 @@ -556,20 +556,19 @@ 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 + In such a situation, it may still be possible to exploit GenericAll permissions on a domain + object. Indeed, with GenericAll permissions over a domain object, it is possible to modify its + gPLink attribute. This may be abused to apply a malicious Group Policy Object + (GPO) to all of the domain's user and computer objects (including the ones located in nested OUs). + This can be exploited to make said child objects execute arbitrary commands e.g. through an immediate scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -586,29 +585,37 @@ const WindowsAbuse: FC = - 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the 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. + 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. ); case 'GPO': return ( <> + + With full control over a GPO, you may make modifications to that GPO in order to inject malicious configurations into it. + You could for instance add a Scheduled Task that will then be executed by all of the computers and/or users to which the GPO applies, + thus compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing + to precisely target a specific object. + GPOs are applied 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 a more detailed write up on this abuse. + + - 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. + On a domain-joined Windows machine, the native Group Policy Management Console (GPMC) may be used to edit GPOs. + On a non-domain joined Windows Machine, the{' '} + + DRSAT (Disconnected RSAT) + tool can be used. ); @@ -697,27 +704,26 @@ 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 such a situation, it may still be possible to exploit GenericAll permissions on an OU. + Indeed, with GenericAll permissions over an OU, it is possible to modify its + gPLink attribute. This may be abused to apply a malicious Group Policy Object + (GPO) to all of the OU's user and computer objects (including the ones located in nested OUs). + This can be exploited to make said child objects execute arbitrary commands e.g. through an immediate + scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -734,16 +740,13 @@ const WindowsAbuse: FC = - 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target OU object 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. + 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. ); @@ -830,6 +833,51 @@ const WindowsAbuse: FC = ); + case 'Site': + return ( + <> + + GenericAll permissions over a Site object allow modifying the gPLink attribute of the site. + The ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious Group Policy Object + (GPO) to all of the objects associated with the site. This can be exploited to make said objects execute + arbitrary commands e.g. through an immediate scheduled task, thus compromising them. + In the case of a site, the affected objects are the computers that have an IP address included in one of the site's subnets + (or computers that do not belong to any site if this is the default site), as well as users connecting to these computers. + Note that Server objects associated with the Site should be located in the Site. + + + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + 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{' '} + + this article + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) in that GPO, and then link the GPO to the target Site through its gPLink attribute. + + + + Be mindful of the number of users and computers that are in the given site as they all will + attempt to fetch 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 8750ebc47c93..da8955cf3f57 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 @@ -180,19 +180,53 @@ 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. + With GenericWrite over a GPO, you may make modifications to that GPO in order to inject malicious configurations into it. + You could for instance add a Scheduled Task that will then be executed by all of the computers and/or users to which the GPO applies, + thus compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing + to precisely target a specific object. + GPOs are applied 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 a more detailed write up on this abuse. - - pyGPOAbuse.py + The + GroupPolicyBackdoor.py {' '} - can be used for that purpose. + tool can be used to perform the attack from a Linux machine. First, define a module file that describes the configuration to inject. + The following one defines a computer configuration, with an immediate Scheduled Task adding a domain user as local administrator. + A filter is defined, so that it only applies 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"}]' + } + + + + Place the described configuration into the Scheduled_task_add.ini file, and 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. This edge can be a false positive in rare scenarios. If you have GenericWrite on the GPO with @@ -216,10 +250,10 @@ const LinuxAbuse: FC = ({ targetType }) => { - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -236,18 +270,31 @@ const LinuxAbuse: FC = ({ targetType }) => { . - - 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target OU through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + You can then link the modified GPO to the OU, through 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. + 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. ); @@ -259,14 +306,14 @@ const LinuxAbuse: FC = ({ targetType }) => { 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. + items execute arbitrary commands through e.g. an immediate scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -283,18 +330,31 @@ const LinuxAbuse: FC = ({ targetType }) => { . - - 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + You can then link the modified GPO to the domain, through 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. + 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. ); @@ -350,6 +410,70 @@ const LinuxAbuse: FC = ({ targetType }) => { ); + case 'Site': + return ( + <> + + GenericWrite permissions over a Site object allow modifying the gPLink attribute of the site. + The ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious Group Policy Object + (GPO) to all of the objects associated with the site. This can be exploited to make said objects execute + arbitrary commands e.g. through an immediate scheduled task, thus compromising them. + In the case of a site, the affected objects are the computers that have an IP address included in one of the site's subnets + (or computers that do not belong to any site if this is the default site), as well as users connecting to these computers. + Note that Server objects associated with the Site should be located in the Site. + + + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + + OUned.py + {' '} + tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + + the article associated to the OUned.py tool + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) in that GPO, and then link the GPO to the target Site through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + Now you can link the modified GPO to the Site object, through 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"' + } + + + + Be mindful of the number of users and computers that are in the given site as they all will + attempt to fetch 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 de5af1bcde5e..f98726c0d851 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 @@ -122,6 +122,12 @@ const References: FC = () => { href='https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53'> https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53 + + https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites + ); }; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx index eb111f2e89c5..bd60cdcd7f05 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx @@ -139,12 +139,23 @@ const WindowsAbuse: FC = ({ 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. + With GenericWrite permissions over a GPO, you may make modifications to that GPO in order to inject malicious configurations into it. + You could for instance add a Scheduled Task that will then be executed by all of the computers and/or users to which the GPO applies, + thus compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing + to precisely target a specific object. + GPOs are applied 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 a more detailed write up on this abuse. + + + + On a domain-joined Windows machine, the native Group Policy Management Console (GPMC) may be used to edit GPOs. + On a non-domain joined Windows Machine, the{' '} + + DRSAT (Disconnected RSAT) + tool can be used. This edge can be a false positive in rare scenarios. If you have GenericWrite on the GPO with @@ -261,18 +272,18 @@ 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. + With GenericWrite permissions over an OU, it is possible to modify its + gPLink attribute. This may be abused to apply a malicious Group Policy Object + (GPO) to all of the OU's user and computer objects (including the ones located in nested OUs). + This can be exploited to make said child objects execute arbitrary commands e.g. through an immediate + scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -289,16 +300,13 @@ const WindowsAbuse: FC = ({ 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target OU object 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. + 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. ); @@ -306,18 +314,18 @@ 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. + With GenericWrite permission over a domain object, it is possible to modify its + gPLink attribute. This may be abused to apply a malicious Group Policy Object + (GPO) to all of the domain's user and computer objects (including the ones located in nested OUs). + This can be exploited to make said child objects execute arbitrary commands e.g. through an immediate + scheduled task, thus compromising them. - 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 have control over an existing GPO (or the ability to create new ones), 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. @@ -334,16 +342,13 @@ const WindowsAbuse: FC = ({ 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 have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the 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. + 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. ); @@ -399,6 +404,51 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t ); + case 'Site': + return ( + <> + + GenericWrite permissions over a Site object allow modifying the gPLink attribute of the site. + The ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious Group Policy Object + (GPO) to all of the objects associated with the site. This can be exploited to make said objects execute + arbitrary commands e.g. through an immediate scheduled task, thus compromising them. + In the case of a site, the affected objects are the computers that have an IP address included in one of the site's subnets + (or computers that do not belong to any site if this is the default site), as well as users connecting to these computers. + Note that Server objects associated with the Site should be located in the Site. + + + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + 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{' '} + + this article + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) in that GPO, and then link the GPO to the target Site through its gPLink attribute. + + + + Be mindful of the number of users and computers that are in the given site as they all will + attempt to fetch 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 d721ea5b302a..143d16771f26 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 @@ -28,24 +28,16 @@ const General: FC = ({ sourceName, sourceType, targetType, target - 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, + The ability to alter the gPLink attribute of an object may allow an attacker to apply a malicious Group Policy Object + (GPO) to all child user and computer objects. This can be + exploited to make said child objects execute arbitrary commands through e.g. an immediate scheduled task, thus compromising them. - - - 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. - - - 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 domain and OU objects, child user/computer objects are the ones belonging to the domain/OU (including the ones located in nested OUs). + In the case of a site, the affected objects are the computers that have an IP address included in one of the site's subnets + (or computers that do not belong to any site if this is the default site), as well as users connecting to these computers. + Note that Server objects associated with the Site should be located in the Site. ); 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 8f106d2deb8f..8aab63491bb6 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 @@ -16,31 +16,175 @@ import { Link, Typography } from '@mui/material'; 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 have control over an existing GPO (or the ability to create new ones), 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. + + + + From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + + OUned.py + {' '} + tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + + the article associated to the OUned.py tool + + . + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + You can then link the modified GPO to the domain, through 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"' + } + + + + 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. + + + ); + case 'OU': + return ( + <> + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + + OUned.py + {' '} + tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + + the article associated to the OUned.py tool + + . + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target OU through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + You can then link the modified GPO to the OU, through 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"' + } + + + + 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. + + + ); + case 'Site': + return ( + <> + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + From a Linux machine, the gPLink manipulation attack vector may be exploited using the{' '} + + OUned.py + {' '} + tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + + the article associated to the OUned.py tool + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) in that GPO, and then link the GPO to the target Site through its gPLink attribute. + To do so, you can use the + GroupPolicyBackdoor.py + {' '} + tool. You may for instance 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"' + } + + + Now you can link the modified GPO to the Site object, through 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"' + } + + + + Be mindful of the number of users and computers that are in the given site as they all will + attempt to fetch 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 8a5ce5fdfa24..502dfcc57483 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,12 @@ 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 e43b110d969e..68aea5001b34 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 @@ -16,29 +16,120 @@ import { Link, Typography } from '@mui/material'; 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 have control over an existing GPO (or the ability to create new ones), 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. + + + + 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{' '} + + this article + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + + + + 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. + + + ); + case 'OU': + return ( + <> + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + 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{' '} + + this article + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) into a controlled GPO, and then link the GPO to the target OU object through its gPLink attribute. + + + + 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. + + + ); + case 'Site': + return ( + <> + + If you do not have control over an existing GPO (or the ability to create new ones), 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. + + + + 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{' '} + + this article + + . + + + + If you have control over an existing GPO (or the ability to create new ones), the attack is simpler. You can inject a malicious + configuration (e.g. an immediate scheduled task) in that GPO, and then link the GPO to the target Site through its gPLink attribute. + + + + Be mindful of the number of users and computers that are in the given site as they all will + attempt to fetch 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 7ede0cf40373..315cdde1fbc8 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; } diff --git a/packages/javascript/bh-shared-ui/src/utils/content.ts b/packages/javascript/bh-shared-ui/src/utils/content.ts index 71be8561fe79..b02ca371767c 100644 --- a/packages/javascript/bh-shared-ui/src/utils/content.ts +++ b/packages/javascript/bh-shared-ui/src/utils/content.ts @@ -161,6 +161,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), Meta: apiClient.getMetaV2, }; @@ -896,6 +901,11 @@ export const allSections: Partial EntityInfo label: 'Users', queryType: 'gpo-users', }, + { + id, + label: 'Sites', + queryType: 'gpo-sites', + }, { id, label: 'Tier Zero Objects', @@ -1020,6 +1030,37 @@ 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 Site Servers', + queryType: 'site-linked_siteservers', + }, + { + id, + label: 'Linked Site Subnets', + queryType: 'site-linked_sitesubnets', + }, + ], + [ActiveDirectoryNodeKind.SiteServer]: (id: string) => [ + { + id, + label: 'Inbound Object Control', + queryType: 'siteserver-inbound_object_control', + }, + ], + [ActiveDirectoryNodeKind.SiteSubnet]: (id: string) => [ + { + id, + label: 'Inbound Object Control', + queryType: 'sitesubnet-inbound_object_control', + }, + ], [ActiveDirectoryNodeKind.User]: (id: string) => [ { id, @@ -1740,6 +1781,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 }) => @@ -1792,6 +1835,20 @@ 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_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), + 'siteserver-inbound_object_control': ({ id, skip, limit, type }) => + apiClient + .getSiteServerControllersV2(id, skip, limit, type, { signal: controller.signal }) + .then((res) => res.data), + 'sitesubnet-inbound_object_control': ({ id, skip, limit, type }) => + apiClient + .getSiteSubnetControllersV2(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 f86a9c94cd9a..5010c789c783 100644 --- a/packages/javascript/bh-shared-ui/src/utils/icons.ts +++ b/packages/javascript/bh-shared-ui/src/utils/icons.ts @@ -35,6 +35,9 @@ import { faLandmark, faList, faLock, + faMapSigns, + faMap, + faMapMarker, faObjectGroup, faQuestion, faRobot, @@ -140,6 +143,21 @@ export const NODE_ICONS: IconDictionary = { color: '#99B2DD', }, + [ActiveDirectoryNodeKind.Site]: { + icon: faMapSigns, + color: '#bababdff', + }, + + [ActiveDirectoryNodeKind.SiteServer]: { + icon: faMapMarker, + color: '#dcdce6ff', + }, + + [ActiveDirectoryNodeKind.SiteSubnet]: { + icon: faMap, + color: '#fdfdfdff', + }, + [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..3d1219ac93e4 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: 'SiteSubnet', kind: ActiveDirectoryNodeKind.SiteSubnet }, containers: { displayText: 'Containers', kind: ActiveDirectoryNodeKind.Container, diff --git a/packages/javascript/js-client-library/src/client.ts b/packages/javascript/js-client-library/src/client.ts index ee77b2c09a47..bbcf64460a6b 100644 --- a/packages/javascript/js-client-library/src/client.ts +++ b/packages/javascript/js-client-library/src/client.ts @@ -1847,6 +1847,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`, @@ -2616,6 +2631,118 @@ 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 + ) + ); + 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 + ) + ); + + getSiteServerControllersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/siteservers/${id}/controllers`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + + getSiteSubnetV2 = (id: string, counts?: boolean, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sitesubnets/${id}`, + Object.assign( + { + params: { + counts, + }, + }, + options + ) + ); + + getSiteSubnetControllersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + this.baseClient.get( + `/api/v2/sitesubnets/${id}/controllers`, + Object.assign( + { + params: { + skip, + limit, + type, + }, + }, + options + ) + ); + getMetaV2 = (id: string, options?: RequestOptions) => this.baseClient.get(`/api/v2/meta/${id}`, options); getShortestPathV2 = (startNode: string, endNode: string, relationshipKinds?: string, options?: RequestOptions) => From 892c148a02e4f8c6ed2383bd0461f41149f748ca Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 12:43:52 +0200 Subject: [PATCH 02/15] feat: update AD site ingest from SharpHoundCommon --- cmd/api/src/services/graphify/convertors.go | 13 ++++--- packages/cue/bh/ad/ad.cue | 10 ++++- packages/go/ein/ad.go | 37 +++++++++++++++++++ packages/go/ein/ad_test.go | 30 +++++++++++++++ packages/go/ein/incoming_models.go | 9 ++++- packages/go/graphschema/ad/ad.go | 9 +++-- packages/go/graphschema/common/common.go | 6 +-- .../bh-shared-ui/src/graphSchema.ts | 5 ++- 8 files changed, 102 insertions(+), 17 deletions(-) diff --git a/cmd/api/src/services/graphify/convertors.go b/cmd/api/src/services/graphify/convertors.go index 35b211f47cf9..431854d63c72 100644 --- a/cmd/api/src/services/graphify/convertors.go +++ b/cmd/api/src/services/graphify/convertors.go @@ -301,7 +301,7 @@ func convertIssuancePolicy(issuancePolicy ein.IssuancePolicy, converted *Convert } func convertSiteData(site ein.Site, converted *ConvertedData, ingestTime time.Time) { - baseNodeProp := ein.ConvertObjectToNode(site.IngestBase, ad.Site, ingestTime) + 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)...) @@ -309,17 +309,21 @@ func convertSiteData(site ein.Site, converted *ConvertedData, ingestTime time.Ti 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(ein.IngestBase(siteServer), ad.SiteServer, ingestTime) + baseNodeProp := ein.ConvertObjectToNode(siteServer.IngestBase, ad.SiteServer, ingestTime) converted.NodeProps = append(converted.NodeProps, baseNodeProp) - converted.RelProps = append(converted.RelProps, ein.ParseACEData(baseNodeProp, siteServer.Aces, siteServer.ObjectIdentifier, ad.SiteServer)...) + converted.RelProps = append(converted.RelProps, ein.ParseSiteServerData(siteServer)...) - if rel := ein.ParseObjectContainer(ein.IngestBase(siteServer), ad.SiteServer); rel.IsValid() { + if rel := ein.ParseObjectContainer(siteServer.IngestBase, ad.SiteServer); rel.IsValid() { converted.RelProps = append(converted.RelProps, rel) } } @@ -327,7 +331,6 @@ func convertSiteServerData(siteServer ein.SiteServer, converted *ConvertedData, 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) - converted.RelProps = append(converted.RelProps, ein.ParseACEData(baseNodeProp, siteSubnet.Aces, siteSubnet.ObjectIdentifier, ad.SiteSubnet)...) if rel := ein.ParseObjectContainer(ein.IngestBase(siteSubnet), ad.SiteSubnet); rel.IsValid() { converted.RelProps = append(converted.RelProps, rel) diff --git a/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index d834cbc2e8dc..4b9f635bf405 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -1364,6 +1364,11 @@ Contains: types.#Kind & { schema: "active_directory" } +ServerIs: types.#Kind & { + symbol: "ServerIs" + schema: "active_directory" +} + GPLink: types.#Kind & { symbol: "GPLink" schema: "active_directory" @@ -1747,6 +1752,7 @@ RelationshipKinds: [ AddMember, HasSession, Contains, + ServerIs, GPLink, AllowedToDelegate, CoerceToTGT, @@ -1918,10 +1924,10 @@ SharedRelationshipKinds: [ ] // Edges that are used during inbound traversal -InboundRelationshipKinds: list.Concat([SharedRelationshipKinds, [Contains]]) +InboundRelationshipKinds: list.Concat([SharedRelationshipKinds, [Contains, ServerIs]]) // Edges that are used during outbound traversal -OutboundRelationshipKinds: list.Concat([SharedRelationshipKinds,[Contains, DCFor]]) +OutboundRelationshipKinds: list.Concat([SharedRelationshipKinds,[Contains, DCFor, ServerIs]]) // Edges that are used in pathfinding PathfindingRelationships: list.Concat([SharedRelationshipKinds,[Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation]]) diff --git a/packages/go/ein/ad.go b/packages/go/ein/ad.go index 489e23aee84b..e365eafd9cea 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) @@ -765,6 +779,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 8c31fbae8122..1c3028a949b5 100644 --- a/packages/go/ein/incoming_models.go +++ b/packages/go/ein/incoming_models.go @@ -176,10 +176,15 @@ type IssuancePolicy struct { type Site struct { IngestBase - Links []GPLink + ChildObjects []TypedPrincipal + Links []GPLink + InheritanceHashes []string } -type SiteServer IngestBase +type SiteServer struct { + IngestBase + ServerIs TypedPrincipal +} type SiteSubnet IngestBase diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 2056efaf1883..c7ea3aca6e67 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// Copyright 2026 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. @@ -55,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") @@ -1150,7 +1151,7 @@ func Nodes() []graph.Kind { 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, WriteOwnerRaw, OwnsLimitedRights, OwnsRaw, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, WriteOwnerRaw, OwnsLimitedRights, OwnsRaw, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, 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} @@ -1159,10 +1160,10 @@ 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, ManageCA, ManageCertificates, Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation} } 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, ManageCA, ManageCertificates, Contains, ServerIs} } 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, 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, SyncedToEntraUser, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToADCS, WriteOwnerLimitedRights, OwnsLimitedRights, ClaimSpecialIdentity, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, ContainsIdentity, PropagatesACEsTo, GPOAppliesTo, CanApplyGPO, HasTrustKeys, ManageCA, ManageCertificates, Contains, DCFor, ServerIs} } 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, SyncedToEntraUser, Owns, WriteOwner, ExtendedByPolicy, CoerceAndRelayNTLMToADCS, CoerceAndRelayNTLMToSMB, CoerceAndRelayNTLMToLDAP, CoerceAndRelayNTLMToLDAPS, GPOAppliesTo, CanApplyGPO, HasTrustKeys} diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index f18f99a486bc..1c2b95429eee 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// Copyright 2026 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. @@ -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.SyncedToEntraUser, 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.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.SyncedToADUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains} + 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.SyncedToEntraUser, 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.ManageCA, ad.ManageCertificates, ad.Contains, ad.ServerIs, 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.SyncedToADUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains} } 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.SyncedToEntraUser, 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.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.SyncedToADUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains} + 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.SyncedToEntraUser, 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.ManageCA, ad.ManageCertificates, ad.Contains, ad.DCFor, ad.ServerIs, 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.SyncedToADUser, azure.AZRoleEligible, azure.AZRoleApprover, azure.Contains} } type Property string diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 315cdde1fbc8..61c3502ee4d4 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -1,4 +1,4 @@ -// Copyright 2025 Specter Ops, Inc. +// Copyright 2026 Specter Ops, Inc. // // Licensed under the Apache License, Version 2.0 // you may not use this file except in compliance with the License. @@ -90,6 +90,7 @@ export enum ActiveDirectoryRelationshipKind { AddMember = 'AddMember', HasSession = 'HasSession', Contains = 'Contains', + ServerIs = 'ServerIs', GPLink = 'GPLink', AllowedToDelegate = 'AllowedToDelegate', CoerceToTGT = 'CoerceToTGT', @@ -190,6 +191,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: From 6880962c9ca2879fbc182eb4975fd8c70942c53b Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 13:29:12 +0200 Subject: [PATCH 03/15] fix: align AD site support with current main BED-8138 --- cmd/api/src/database/dataquality.go | 6 + .../src/services/dataquality/dataquality.go | 12 + cmd/ui/src/ducks/graph/graphutils.ts | 6 +- packages/go/analysis/ad/queries.go | 10 +- packages/go/openapi/doc/openapi.json | 495 ++++++++++++++++++ packages/go/openapi/src/openapi.yaml | 27 + .../openapi/src/paths/gpos.gpos.id.sites.yaml | 45 ++ .../src/paths/sites.sites.id.controllers.yaml | 45 ++ .../src/paths/sites.sites.id.siteservers.yaml | 45 ++ .../src/paths/sites.sites.id.sitesubnets.yaml | 45 ++ .../go/openapi/src/paths/sites.sites.id.yaml | 44 ++ ...iteservers.siteservers.id.controllers.yaml | 45 ++ .../src/paths/siteservers.siteservers.id.yaml | 44 ++ ...itesubnets.sitesubnets.id.controllers.yaml | 45 ++ .../src/paths/sitesubnets.sitesubnets.id.yaml | 44 ++ .../model.ad-data-quality-aggregation.yaml | 8 + .../schemas/model.ad-data-quality-stat.yaml | 8 + 17 files changed, 966 insertions(+), 8 deletions(-) create mode 100644 packages/go/openapi/src/paths/gpos.gpos.id.sites.yaml create mode 100644 packages/go/openapi/src/paths/sites.sites.id.controllers.yaml create mode 100644 packages/go/openapi/src/paths/sites.sites.id.siteservers.yaml create mode 100644 packages/go/openapi/src/paths/sites.sites.id.sitesubnets.yaml create mode 100644 packages/go/openapi/src/paths/sites.sites.id.yaml create mode 100644 packages/go/openapi/src/paths/siteservers.siteservers.id.controllers.yaml create mode 100644 packages/go/openapi/src/paths/siteservers.siteservers.id.yaml create mode 100644 packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.controllers.yaml create mode 100644 packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.yaml 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/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/ui/src/ducks/graph/graphutils.ts b/cmd/ui/src/ducks/graph/graphutils.ts index f2617812b0d3..898a1f2a4445 100644 --- a/cmd/ui/src/ducks/graph/graphutils.ts +++ b/cmd/ui/src/ducks/graph/graphutils.ts @@ -236,9 +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-clipboard-check', - [GraphNodeTypes.SiteServer]: 'fa-clipboard-check', - [GraphNodeTypes.SiteSubnet]: '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/packages/go/analysis/ad/queries.go b/packages/go/analysis/ad/queries.go index 59c956bbd095..33abfacd0a2c 100644 --- a/packages/go/analysis/ad/queries.go +++ b/packages/go/analysis/ad/queries.go @@ -454,7 +454,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), ) }, @@ -479,8 +479,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) { @@ -531,7 +531,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), ) }, @@ -556,7 +556,7 @@ func FetchEnforcedGPOsPaths(ctx context.Context, db graph.Database, target *grap // 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, ad.Site) { - // If we run into anything that isn't an OU or a Domain node then we're done checking for + // 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) { diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 96e8c9bded96..9a91b72cf149 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,420 @@ } } }, + "/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}/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/siteservers/{object_id}/controllers": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteServerEntityControllers", + "summary": "Get Site Server entity controllers", + "description": "Get a list, graph, or count of the principals that can control this Site Server.", + "tags": [ + "Site Servers", + "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/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/sitesubnets/{object_id}/controllers": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "$ref": "#/components/parameters/path.object-id" + } + ], + "get": { + "operationId": "GetSiteSubnetEntityControllers", + "summary": "Get Site Subnet entity controllers", + "description": "Get a list, graph, or count of the principals that can control this Site Subnet.", + "tags": [ + "Site Subnets", + "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/ous/{object_id}": { "parameters": [ { @@ -21756,6 +22224,18 @@ "certtemplates": { "type": "integer" }, + "issuancepolicies": { + "type": "integer" + }, + "sites": { + "type": "integer" + }, + "siteservers": { + "type": "integer" + }, + "sitesubnets": { + "type": "integer" + }, "acls": { "type": "integer" }, @@ -21884,6 +22364,18 @@ "certtemplates": { "type": "integer" }, + "issuancepolicies": { + "type": "integer" + }, + "sites": { + "type": "integer" + }, + "siteservers": { + "type": "integer" + }, + "sitesubnets": { + "type": "integer" + }, "acls": { "type": "integer" }, @@ -22959,6 +23451,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..3b2985cadc0c 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,28 @@ 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}/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' + /api/v2/siteservers/{object_id}/controllers: + $ref: './paths/siteservers.siteservers.id.controllers.yaml' + + # site subnets + /api/v2/sitesubnets/{object_id}: + $ref: './paths/sitesubnets.sitesubnets.id.yaml' + /api/v2/sitesubnets/{object_id}/controllers: + $ref: './paths/sitesubnets.sitesubnets.id.controllers.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.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.controllers.yaml b/packages/go/openapi/src/paths/siteservers.siteservers.id.controllers.yaml new file mode 100644 index 000000000000..00501ee14b3f --- /dev/null +++ b/packages/go/openapi/src/paths/siteservers.siteservers.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: GetSiteServerEntityControllers + summary: Get Site Server entity controllers + description: Get a list, graph, or count of the principals that can control this Site Server. + tags: + - Site Servers + - 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/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.controllers.yaml b/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.controllers.yaml new file mode 100644 index 000000000000..9015fa7f5668 --- /dev/null +++ b/packages/go/openapi/src/paths/sitesubnets.sitesubnets.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: GetSiteSubnetEntityControllers + summary: Get Site Subnet entity controllers + description: Get a list, graph, or count of the principals that can control this Site Subnet. + tags: + - Site Subnets + - 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/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: From 531be7e3c262fce0cd3b354cea3d78ee53751541 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 14:58:53 +0200 Subject: [PATCH 04/15] fix entity panels --- cmd/api/src/api/registration/v2.go | 2 -- cmd/api/src/api/v2/ad_entity.go | 10 ++++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index 31d9632f0ae9..27f562261aff 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -356,11 +356,9 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) { // SiteServer Entity API routerInst.GET(fmt.Sprintf("/api/v2/siteservers/{%s}", api.URIPathVariableObjectID), resources.GetSiteServerEntityInfo).RequirePermissions(permissions.GraphDBRead), - routerInst.GET(fmt.Sprintf("/api/v2/siteservers/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead), // SiteSubnet Entity API routerInst.GET(fmt.Sprintf("/api/v2/sitesubnets/{%s}", api.URIPathVariableObjectID), resources.GetSiteSubnetEntityInfo).RequirePermissions(permissions.GraphDBRead), - routerInst.GET(fmt.Sprintf("/api/v2/sitesubnets/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).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), diff --git a/cmd/api/src/api/v2/ad_entity.go b/cmd/api/src/api/v2/ad_entity.go index 4313e5d79f2a..ec73d325c411 100644 --- a/cmd/api/src/api/v2/ad_entity.go +++ b/cmd/api/src/api/v2/ad_entity.go @@ -304,6 +304,8 @@ func (s *Resources) GetSiteEntityInfo(response http.ResponseWriter, request *htt var ( countQueries = map[string]any{ "controllers": adAnalysis.FetchInboundADEntityControllers, + "siteServers": adAnalysis.CreateSiteContainedListDelegate(ad.SiteServer), + "siteSubnets": adAnalysis.CreateSiteContainedListDelegate(ad.SiteSubnet), } ) @@ -312,9 +314,7 @@ func (s *Resources) GetSiteEntityInfo(response http.ResponseWriter, request *htt func (s *Resources) GetSiteServerEntityInfo(response http.ResponseWriter, request *http.Request) { var ( - countQueries = map[string]any{ - "controllers": adAnalysis.FetchInboundADEntityControllers, - } + countQueries = map[string]any{} ) s.handleAdEntityInfoQuery(response, request, ad.SiteServer, countQueries) @@ -322,9 +322,7 @@ func (s *Resources) GetSiteServerEntityInfo(response http.ResponseWriter, reques func (s *Resources) GetSiteSubnetEntityInfo(response http.ResponseWriter, request *http.Request) { var ( - countQueries = map[string]any{ - "controllers": adAnalysis.FetchInboundADEntityControllers, - } + countQueries = map[string]any{} ) s.handleAdEntityInfoQuery(response, request, ad.SiteSubnet, countQueries) From 8185430147188dcfff2aa05d411c5816c011c95f Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 15:41:08 +0200 Subject: [PATCH 05/15] pz rule for dc site servers --- ..._v9_add_tier_zero_site_server_selector.sql | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 cmd/api/src/database/migration/migrations/20260615130000_v9_add_tier_zero_site_server_selector.sql 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'; From 52aa6a13f802287a353201ddbd6dd02a21deaee4 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 15:48:26 +0200 Subject: [PATCH 06/15] make ServerIs traversable --- .../src/database/migration/extensions/ad_graph_schema.sql | 2 +- packages/cue/bh/ad/ad.cue | 5 +++-- packages/go/graphschema/ad/ad.go | 8 ++++---- packages/go/graphschema/common/common.go | 4 ++-- packages/javascript/bh-shared-ui/src/graphSchema.ts | 2 ++ 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql index 2fd3441b81ef..052a40925c2e 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -277,7 +277,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', '', false); + 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/packages/cue/bh/ad/ad.cue b/packages/cue/bh/ad/ad.cue index 9df016ec692c..981b76688ba9 100644 --- a/packages/cue/bh/ad/ad.cue +++ b/packages/cue/bh/ad/ad.cue @@ -1943,13 +1943,14 @@ SharedRelationshipKinds: [ WritePublicInformation, ManageCA, ManageCertificates, + ServerIs, ] // Edges that are used during inbound traversal -InboundRelationshipKinds: list.Concat([SharedRelationshipKinds, [Contains, ServerIs]]) +InboundRelationshipKinds: list.Concat([SharedRelationshipKinds, [Contains]]) // Edges that are used during outbound traversal -OutboundRelationshipKinds: list.Concat([SharedRelationshipKinds,[Contains, DCFor, ServerIs]]) +OutboundRelationshipKinds: list.Concat([SharedRelationshipKinds,[Contains, DCFor]]) // Edges that are used in pathfinding PathfindingRelationships: list.Concat([SharedRelationshipKinds,[Contains, DCFor, SameForestTrust, SpoofSIDHistory, AbuseTGTDelegation]]) diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 53c7533cfe08..93f0ecc170d9 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -1162,16 +1162,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, ServerIs} + 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, ServerIs} + 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} diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index 84e12bed6186..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, ad.ServerIs, 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, ad.ServerIs, 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/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 1af55301b85d..914de7a4c707 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -862,6 +862,7 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi ActiveDirectoryRelationshipKind.WritePublicInformation, ActiveDirectoryRelationshipKind.ManageCA, ActiveDirectoryRelationshipKind.ManageCertificates, + ActiveDirectoryRelationshipKind.ServerIs, ActiveDirectoryRelationshipKind.Contains, ActiveDirectoryRelationshipKind.DCFor, ActiveDirectoryRelationshipKind.SameForestTrust, @@ -926,6 +927,7 @@ export function ActiveDirectoryPathfindingEdgesMatchFrontend(): ActiveDirectoryR ActiveDirectoryRelationshipKind.WritePublicInformation, ActiveDirectoryRelationshipKind.ManageCA, ActiveDirectoryRelationshipKind.ManageCertificates, + ActiveDirectoryRelationshipKind.ServerIs, ActiveDirectoryRelationshipKind.Contains, ActiveDirectoryRelationshipKind.DCFor, ActiveDirectoryRelationshipKind.SameForestTrust, From 5fa500f22c200bb2af57ae438eabcf463a48ee3f Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 15:52:39 +0200 Subject: [PATCH 07/15] fix site nodes entity panels --- cmd/api/src/api/registration/v2.go | 1 + cmd/api/src/api/v2/ad_entity.go | 1 + cmd/api/src/api/v2/ad_related_entity.go | 6 +- cmd/api/src/api/v2/ad_related_entity_test.go | 8 ++ .../go/analysis/ad/ad_integration_test.go | 85 +++++++++++++- packages/go/analysis/ad/queries.go | 27 +++++ packages/go/openapi/doc/openapi.json | 108 +++++------------- packages/go/openapi/src/openapi.yaml | 6 +- ...s.yaml => sites.sites.id.linked-gpos.yaml} | 10 +- ...itesubnets.sitesubnets.id.controllers.yaml | 45 -------- .../bh-shared-ui/src/utils/content.ts | 31 ++--- .../js-client-library/src/client.ts | 38 ++---- 12 files changed, 180 insertions(+), 186 deletions(-) rename packages/go/openapi/src/paths/{siteservers.siteservers.id.controllers.yaml => sites.sites.id.linked-gpos.yaml} (82%) delete mode 100644 packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.controllers.yaml diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index 27f562261aff..5791d5c3d52a 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -351,6 +351,7 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) { // 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), diff --git a/cmd/api/src/api/v2/ad_entity.go b/cmd/api/src/api/v2/ad_entity.go index ec73d325c411..63fa96e99367 100644 --- a/cmd/api/src/api/v2/ad_entity.go +++ b/cmd/api/src/api/v2/ad_entity.go @@ -304,6 +304,7 @@ func (s *Resources) GetSiteEntityInfo(response http.ResponseWriter, request *htt var ( countQueries = map[string]any{ "controllers": adAnalysis.FetchInboundADEntityControllers, + "linkedgpos": adAnalysis.FetchEntityLinkedGPOList, "siteServers": adAnalysis.CreateSiteContainedListDelegate(ad.SiteServer), "siteSubnets": adAnalysis.CreateSiteContainedListDelegate(ad.SiteSubnet), } diff --git a/cmd/api/src/api/v2/ad_related_entity.go b/cmd/api/src/api/v2/ad_related_entity.go index f84541601075..40f5090bcaee 100644 --- a/cmd/api/src/api/v2/ad_related_entity.go +++ b/cmd/api/src/api/v2/ad_related_entity.go @@ -226,7 +226,7 @@ func (s *Resources) ListADGPOAffectedUsers(response http.ResponseWriter, request } func (s *Resources) ListADGPOAffectedSites(response http.ResponseWriter, request *http.Request) { - s.handleAdRelatedEntityQuery(response, request, "ListADGPOAffectedSites", adAnalysis.FetchGPOAffectedSitePaths, adAnalysis.CreateGPOAffectedIntermediariesListDelegate(adAnalysis.SelectSitesCandidateFilter)) + s.handleAdRelatedEntityQuery(response, request, "ListADGPOAffectedSites", adAnalysis.FetchGPOAffectedSitePaths, adAnalysis.FetchGPOAffectedSites) } func (s *Resources) ListADGPOAffectedComputers(response http.ResponseWriter, request *http.Request) { @@ -269,6 +269,10 @@ func (s *Resources) ListADSiteLinkedServers(response http.ResponseWriter, reques 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/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/queries.go b/packages/go/analysis/ad/queries.go index 33abfacd0a2c..acff0c2d447f 100644 --- a/packages/go/analysis/ad/queries.go +++ b/packages/go/analysis/ad/queries.go @@ -403,6 +403,33 @@ func FetchGPOAffectedSitePaths(tx graph.Transaction, node *graph.Node) (graph.Pa } } +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() diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 4dd5e227a16b..d5db50bb5ff5 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -12272,7 +12272,7 @@ } } }, - "/api/v2/sites/{object_id}/siteservers": { + "/api/v2/sites/{object_id}/linked-gpos": { "parameters": [ { "$ref": "#/components/parameters/header.prefer" @@ -12282,9 +12282,9 @@ } ], "get": { - "operationId": "GetSiteEntitySiteServers", - "summary": "Get Site entity site servers", - "description": "Get a list, graph, or count of the site servers linked to this Site.", + "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", @@ -12326,7 +12326,7 @@ } } }, - "/api/v2/sites/{object_id}/sitesubnets": { + "/api/v2/sites/{object_id}/siteservers": { "parameters": [ { "$ref": "#/components/parameters/header.prefer" @@ -12336,9 +12336,9 @@ } ], "get": { - "operationId": "GetSiteEntitySiteSubnets", - "summary": "Get Site entity site subnets", - "description": "Get a list, graph, or count of the site subnets linked to this Site.", + "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", @@ -12380,55 +12380,7 @@ } } }, - "/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/siteservers/{object_id}/controllers": { + "/api/v2/sites/{object_id}/sitesubnets": { "parameters": [ { "$ref": "#/components/parameters/header.prefer" @@ -12438,11 +12390,11 @@ } ], "get": { - "operationId": "GetSiteServerEntityControllers", - "summary": "Get Site Server entity controllers", - "description": "Get a list, graph, or count of the principals that can control this Site Server.", + "operationId": "GetSiteEntitySiteSubnets", + "summary": "Get Site entity site subnets", + "description": "Get a list, graph, or count of the site subnets linked to this Site.", "tags": [ - "Site Servers", + "Sites", "Community", "Enterprise" ], @@ -12482,7 +12434,7 @@ } } }, - "/api/v2/sitesubnets/{object_id}": { + "/api/v2/siteservers/{object_id}": { "parameters": [ { "$ref": "#/components/parameters/header.prefer" @@ -12492,11 +12444,11 @@ } ], "get": { - "operationId": "GetSiteSubnetEntity", - "summary": "Get Site Subnet entity info", - "description": "Get info and counts for this Site Subnet node.", + "operationId": "GetSiteServerEntity", + "summary": "Get Site Server entity info", + "description": "Get info and counts for this Site Server node.", "tags": [ - "Site Subnets", + "Site Servers", "Community", "Enterprise" ], @@ -12530,7 +12482,7 @@ } } }, - "/api/v2/sitesubnets/{object_id}/controllers": { + "/api/v2/sitesubnets/{object_id}": { "parameters": [ { "$ref": "#/components/parameters/header.prefer" @@ -12540,9 +12492,9 @@ } ], "get": { - "operationId": "GetSiteSubnetEntityControllers", - "summary": "Get Site Subnet entity controllers", - "description": "Get a list, graph, or count of the principals that can control this Site Subnet.", + "operationId": "GetSiteSubnetEntity", + "summary": "Get Site Subnet entity info", + "description": "Get info and counts for this Site Subnet node.", "tags": [ "Site Subnets", "Community", @@ -12550,21 +12502,12 @@ ], "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" + "$ref": "#/components/parameters/query.hydrate-counts" } ], "responses": { "200": { - "$ref": "#/components/responses/related-entity-query-results" + "$ref": "#/components/responses/entity-info-query-results" }, "400": { "$ref": "#/components/responses/bad-request" @@ -12575,6 +12518,9 @@ "403": { "$ref": "#/components/responses/forbidden" }, + "404": { + "$ref": "#/components/responses/not-found" + }, "429": { "$ref": "#/components/responses/too-many-requests" }, diff --git a/packages/go/openapi/src/openapi.yaml b/packages/go/openapi/src/openapi.yaml index 3b2985cadc0c..e1281ba586b1 100644 --- a/packages/go/openapi/src/openapi.yaml +++ b/packages/go/openapi/src/openapi.yaml @@ -558,6 +558,8 @@ paths: $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: @@ -566,14 +568,10 @@ paths: # site servers /api/v2/siteservers/{object_id}: $ref: './paths/siteservers.siteservers.id.yaml' - /api/v2/siteservers/{object_id}/controllers: - $ref: './paths/siteservers.siteservers.id.controllers.yaml' # site subnets /api/v2/sitesubnets/{object_id}: $ref: './paths/sitesubnets.sitesubnets.id.yaml' - /api/v2/sitesubnets/{object_id}/controllers: - $ref: './paths/sitesubnets.sitesubnets.id.controllers.yaml' # ous /api/v2/ous/{object_id}: diff --git a/packages/go/openapi/src/paths/siteservers.siteservers.id.controllers.yaml b/packages/go/openapi/src/paths/sites.sites.id.linked-gpos.yaml similarity index 82% rename from packages/go/openapi/src/paths/siteservers.siteservers.id.controllers.yaml rename to packages/go/openapi/src/paths/sites.sites.id.linked-gpos.yaml index 00501ee14b3f..e42339ff7b3d 100644 --- a/packages/go/openapi/src/paths/siteservers.siteservers.id.controllers.yaml +++ b/packages/go/openapi/src/paths/sites.sites.id.linked-gpos.yaml @@ -7,7 +7,7 @@ # 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, +# 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. @@ -18,11 +18,11 @@ parameters: - $ref: './../parameters/header.prefer.yaml' - $ref: './../parameters/path.object-id.yaml' get: - operationId: GetSiteServerEntityControllers - summary: Get Site Server entity controllers - description: Get a list, graph, or count of the principals that can control this Site Server. + operationId: GetSiteEntityLinkedGpos + summary: Get Site entity linked GPOs + description: Get a list, graph, or count of the GPOs linked to this Site. tags: - - Site Servers + - Sites - Community - Enterprise parameters: diff --git a/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.controllers.yaml b/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.controllers.yaml deleted file mode 100644 index 9015fa7f5668..000000000000 --- a/packages/go/openapi/src/paths/sitesubnets.sitesubnets.id.controllers.yaml +++ /dev/null @@ -1,45 +0,0 @@ -# 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: GetSiteSubnetEntityControllers - summary: Get Site Subnet entity controllers - description: Get a list, graph, or count of the principals that can control this Site Subnet. - tags: - - Site Subnets - - 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/javascript/bh-shared-ui/src/utils/content.ts b/packages/javascript/bh-shared-ui/src/utils/content.ts index e7de730cb596..d09904c6f38f 100644 --- a/packages/javascript/bh-shared-ui/src/utils/content.ts +++ b/packages/javascript/bh-shared-ui/src/utils/content.ts @@ -1062,6 +1062,11 @@ export const allSections: Partial EntityInfo label: 'Inbound Object Control', queryType: 'site-inbound_object_control', }, + { + id, + label: 'Linked GPOs', + queryType: 'site-linked_gpos', + }, { id, label: 'Linked Site Servers', @@ -1073,20 +1078,8 @@ export const allSections: Partial EntityInfo queryType: 'site-linked_sitesubnets', }, ], - [ActiveDirectoryNodeKind.SiteServer]: (id: string) => [ - { - id, - label: 'Inbound Object Control', - queryType: 'siteserver-inbound_object_control', - }, - ], - [ActiveDirectoryNodeKind.SiteSubnet]: (id: string) => [ - { - id, - label: 'Inbound Object Control', - queryType: 'sitesubnet-inbound_object_control', - }, - ], + [ActiveDirectoryNodeKind.SiteServer]: (id: string) => [], + [ActiveDirectoryNodeKind.SiteSubnet]: (id: string) => [], [ActiveDirectoryNodeKind.User]: (id: string) => [ { id, @@ -1880,18 +1873,12 @@ export const entityRelationshipEndpoints = { .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), - 'siteserver-inbound_object_control': ({ id, skip, limit, type }) => - apiClient - .getSiteServerControllersV2(id, skip, limit, type, { signal: controller.signal }) - .then((res) => res.data), - 'sitesubnet-inbound_object_control': ({ id, skip, limit, type }) => - apiClient - .getSiteSubnetControllersV2(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/js-client-library/src/client.ts b/packages/javascript/js-client-library/src/client.ts index 4d37fd8dffd5..c9cbd7e9cca3 100644 --- a/packages/javascript/js-client-library/src/client.ts +++ b/packages/javascript/js-client-library/src/client.ts @@ -2665,9 +2665,9 @@ class BHEAPIClient { options ) ); - getSiteLinkedServersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + getSiteLinkedGPOsV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => this.baseClient.get( - `/api/v2/sites/${id}/siteservers`, + `/api/v2/sites/${id}/linked-gpos`, Object.assign( { params: { @@ -2679,9 +2679,9 @@ class BHEAPIClient { options ) ); - getSiteLinkedSubnetsV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + getSiteLinkedServersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => this.baseClient.get( - `/api/v2/sites/${id}/sitesubnets`, + `/api/v2/sites/${id}/siteservers`, Object.assign( { params: { @@ -2693,23 +2693,9 @@ class BHEAPIClient { options ) ); - - getSiteServerV2 = (id: string, counts?: boolean, options?: RequestOptions) => - this.baseClient.get( - `/api/v2/siteservers/${id}`, - Object.assign( - { - params: { - counts, - }, - }, - options - ) - ); - - getSiteServerControllersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + getSiteLinkedSubnetsV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => this.baseClient.get( - `/api/v2/siteservers/${id}/controllers`, + `/api/v2/sites/${id}/sitesubnets`, Object.assign( { params: { @@ -2722,9 +2708,9 @@ class BHEAPIClient { ) ); - getSiteSubnetV2 = (id: string, counts?: boolean, options?: RequestOptions) => + getSiteServerV2 = (id: string, counts?: boolean, options?: RequestOptions) => this.baseClient.get( - `/api/v2/sitesubnets/${id}`, + `/api/v2/siteservers/${id}`, Object.assign( { params: { @@ -2735,15 +2721,13 @@ class BHEAPIClient { ) ); - getSiteSubnetControllersV2 = (id: string, skip?: number, limit?: number, type?: string, options?: RequestOptions) => + getSiteSubnetV2 = (id: string, counts?: boolean, options?: RequestOptions) => this.baseClient.get( - `/api/v2/sitesubnets/${id}/controllers`, + `/api/v2/sitesubnets/${id}`, Object.assign( { params: { - skip, - limit, - type, + counts, }, }, options From acc2bbde28ded15984ae97466e81aa759db03242 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Tue, 16 Jun 2026 11:23:13 +0200 Subject: [PATCH 08/15] Add cypher queries --- .../bh-shared-ui/src/commonSearchesAGI.ts | 31 +++++++++++++++++++ .../bh-shared-ui/src/commonSearchesAGT.ts | 31 +++++++++++++++++++ 2 files changed, 62 insertions(+) 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..9d5ad9be5515 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, From 8431191fbbef8498303cd0bb7b7f6f0c79851efd Mon Sep 17 00:00:00 2001 From: JonasBK Date: Tue, 16 Jun 2026 11:37:24 +0200 Subject: [PATCH 09/15] better colors --- packages/javascript/bh-shared-ui/src/utils/icons.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/javascript/bh-shared-ui/src/utils/icons.ts b/packages/javascript/bh-shared-ui/src/utils/icons.ts index 720fcdea9453..4e2ac6eb427c 100644 --- a/packages/javascript/bh-shared-ui/src/utils/icons.ts +++ b/packages/javascript/bh-shared-ui/src/utils/icons.ts @@ -130,17 +130,17 @@ export const NODE_ICONS: IconDictionary = { [ActiveDirectoryNodeKind.Site]: { icon: faMapSigns, - color: '#bababdff', + color: '#2DD4BF', }, [ActiveDirectoryNodeKind.SiteServer]: { icon: faMapMarker, - color: '#dcdce6ff', + color: '#60A5FA', }, [ActiveDirectoryNodeKind.SiteSubnet]: { icon: faMap, - color: '#fdfdfdff', + color: '#F59E0B', }, [ActiveDirectoryNodeKind.OU]: { From 9598895e1cf5edaea58c82db1c8ef72b750a94ac Mon Sep 17 00:00:00 2001 From: JonasBK Date: Tue, 16 Jun 2026 16:04:23 +0200 Subject: [PATCH 10/15] add ServerIs refs in entity panel --- cmd/api/src/api/v2/ad_entity.go | 35 ++++++- cmd/api/src/api/v2/ad_entity_test.go | 96 +++++++++++++++++++ cmd/api/src/queries/graph.go | 42 ++++++++ cmd/api/src/queries/mocks/graph.go | 15 +++ packages/csharp/graphschema/PropertyNames.cs | 1 + packages/cue/bh/ad/ad.cue | 8 ++ packages/go/graphschema/ad/ad.go | 9 +- .../bh-shared-ui/src/graphSchema.ts | 3 + .../views/Explore/BasicObjectInfoFields.tsx | 33 ++++++- .../src/views/Explore/fragments.tsx | 4 + 10 files changed, 240 insertions(+), 6 deletions(-) diff --git a/cmd/api/src/api/v2/ad_entity.go b/cmd/api/src/api/v2/ad_entity.go index 63fa96e99367..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{ 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/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/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 981b76688ba9..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, ] diff --git a/packages/go/graphschema/ad/ad.go b/packages/go/graphschema/ad/ad.go index 93f0ecc170d9..20dd37f68edf 100644 --- a/packages/go/graphschema/ad/ad.go +++ b/packages/go/graphschema/ad/ad.go @@ -276,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 { @@ -561,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": @@ -847,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: @@ -1133,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: diff --git a/packages/javascript/bh-shared-ui/src/graphSchema.ts b/packages/javascript/bh-shared-ui/src/graphSchema.ts index 914de7a4c707..00ddd59471d3 100644 --- a/packages/javascript/bh-shared-ui/src/graphSchema.ts +++ b/packages/javascript/bh-shared-ui/src/graphSchema.ts @@ -512,6 +512,7 @@ export enum ActiveDirectoryKindProperties { NetBIOS = 'netbios', AdminSDHolderProtected = 'adminsdholderprotected', ServicePrincipalNames = 'serviceprincipalnames', + Serverreference = 'serverreference', GPOStatusRaw = 'gpostatusraw', GPOStatus = 'gpostatus', } @@ -793,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: 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..9c3b9751ebd3 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,12 @@ 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 +99,24 @@ export const BasicObjectInfoFields: React.FC = (prop props.service_principal_id, props.name )} + {props.serverreferencecomputer && + RelatedKindField( + props.handleSourceNodeSelected, + 'Server Reference 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/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, From 5bee57562cf279dda6bda8471f889fc5523a5ad8 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Tue, 23 Jun 2026 10:59:04 +0200 Subject: [PATCH 11/15] improve edge texts --- .../bh-shared-ui/src/commonSearchesAGT.ts | 2 +- .../components/HelpTexts/GPLink/General.tsx | 11 ++ .../HelpTexts/GPLink/LinuxAbuse.tsx | 24 ++- .../HelpTexts/GPLink/WindowsAbuse.tsx | 19 +- .../HelpTexts/GenericAll/LinuxAbuse.tsx | 164 ++++++++---------- .../HelpTexts/GenericAll/References.tsx | 1 + .../HelpTexts/GenericAll/WindowsAbuse.tsx | 137 +++++++-------- .../HelpTexts/GenericWrite/LinuxAbuse.tsx | 161 ++++++++--------- .../HelpTexts/GenericWrite/WindowsAbuse.tsx | 133 +++++++------- .../HelpTexts/WriteGPLink/General.tsx | 22 ++- .../HelpTexts/WriteGPLink/LinuxAbuse.tsx | 98 +++++------ .../HelpTexts/WriteGPLink/References.tsx | 1 + .../HelpTexts/WriteGPLink/WindowsAbuse.tsx | 79 ++++----- .../src/views/DataQuality/DomainInfo.tsx | 2 +- .../views/Explore/BasicObjectInfoFields.tsx | 6 +- 15 files changed, 414 insertions(+), 446 deletions(-) diff --git a/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts b/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts index 9d5ad9be5515..9a5b2e7ef782 100644 --- a/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts +++ b/packages/javascript/bh-shared-ui/src/commonSearchesAGT.ts @@ -91,7 +91,7 @@ export const CommonSearches: CommonSearchType[] = [ 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`, - } + }, ], }, { 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 816e7e2d64e7..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,12 +23,11 @@ const LinuxAbuse: FC = () => { return ( <> - If you control a GPO linked to a target object, you may make modifications to that GPO in order to - inject malicious configurations into it. You could for instance add a Scheduled Task that will then be - executed by all of the computers and/or users to which the GPO applies, thus compromising them. Note - that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing to precisely - target a specific object. GPOs are applied every 90 minutes for standard objects (with a random offset - of 0 to 30 minutes), and every 5 minutes for domain controllers. + 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. @@ -36,10 +35,9 @@ const LinuxAbuse: FC = () => { GroupPolicyBackdoor.py {' '} - tool can be used to perform the attack from a Linux machine. First, define a module file that describes - the configuration to inject. The following one defines a computer configuration, with an immediate - Scheduled Task adding a domain user as local administrator. A filter is defined, so that it only applies - to a specific target. + 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. @@ -57,8 +55,8 @@ const LinuxAbuse: FC = () => { - Place the described configuration into the Scheduled_task_add.ini file, and inject it into the target - GPO with the 'inject' command. + Save this configuration as Scheduled_task_add.ini, then inject it into the target GPO with the 'inject' + command. { @@ -71,7 +69,7 @@ const LinuxAbuse: FC = () => { 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 1013f0898a4b..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 @@ -23,22 +23,21 @@ const WindowsAbuse: FC = () => { return ( <> - If you control a GPO linked to a target object, you may make modifications to that GPO in order to - inject malicious configurations into it. You could for instance add a Scheduled Task that will then be - executed by all of the computers and/or users to which the GPO applies, thus compromising them. Note - that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing to precisely - target a specific object. GPOs are applied 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 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, the native Group Policy Management Console (GPMC) may be used to - edit GPOs. On a non-domain joined Windows Machine, the{' '} + 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 can be used. + 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 49efcafcf4f8..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,45 @@ const LinuxAbuse: FC = ( - In such a situation, it may still be possible to exploit GenericAll permissions on a domain - object. Indeed, with GenericAll permissions over a domain object, it is possible to modify its - gPLink attribute. This may be abused to apply a malicious Group Policy Object (GPO) to all of - the domain's user and computer objects (including the ones located in nested OUs). This can be - exploited to make said child objects execute arbitrary commands e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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 . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. - To do so, you can use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -448,7 +445,7 @@ const LinuxAbuse: FC = ( } - You can then link the modified GPO to the domain, through the 'link' command. + Then link the modified GPO to the domain with the 'link' command. { @@ -457,8 +454,8 @@ const LinuxAbuse: FC = ( - 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. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -466,13 +463,12 @@ const LinuxAbuse: FC = ( return ( <> - With full control over a GPO, you may make modifications to that GPO in order to inject - malicious configurations into it. You could for instance add a Scheduled Task that will then be - executed by all of the computers and/or users to which the GPO applies, thus compromising them. - Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing - to precisely target a specific object. GPOs are applied 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 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. @@ -483,10 +479,10 @@ const LinuxAbuse: FC = ( href='https://github.com/synacktiv/GroupPolicyBackdoor'> GroupPolicyBackdoor.py {' '} - tool can be used to perform the attack from a Linux machine. First, define a module file that - describes the configuration to inject. The following one defines a computer configuration, with - an immediate Scheduled Task adding a domain user as local administrator. A filter is defined, so - that it only applies to a specific target. + 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. @@ -504,8 +500,8 @@ const LinuxAbuse: FC = ( - Place the described configuration into the Scheduled_task_add.ini file, and inject it into the - target GPO with the 'inject' command. + Save this configuration as Scheduled_task_add.ini, then inject it into the target GPO with the + 'inject' command. { @@ -518,7 +514,7 @@ const LinuxAbuse: FC = ( pyGPOAbuse.py {' '} - can be used for that purpose. + can also be used for this purpose. ); @@ -568,48 +564,45 @@ const LinuxAbuse: FC = ( - In such a situation, it may still be possible to exploit GenericAll permissions on an OU object. - Indeed, with GenericAll permissions over an OU, it is possible to modify its gPLink attribute. - This may be abused to apply a malicious Group Policy Object (GPO) to all of the OU's user and - computer objects (including the ones located in nested OUs). This can be exploited to make said - child objects execute arbitrary commands e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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 . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target OU through its gPLink attribute. To do so, - you can use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -617,7 +610,7 @@ const LinuxAbuse: FC = ( } - You can then link the modified GPO to the OU, through the 'link' command. + Then link the modified GPO to the OU with the 'link' command. { @@ -626,8 +619,8 @@ const LinuxAbuse: FC = ( - 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. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -724,51 +717,46 @@ const LinuxAbuse: FC = ( return ( <> - GenericAll permissions over a Site object allow modifying the gPLink attribute of the site. The - ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious Group - Policy Object (GPO) to all of the objects associated with the site. This can be exploited to - make said objects execute arbitrary commands e.g. through an immediate scheduled task, thus - compromising them. In the case of a site, the affected objects are the computers that have an IP - address included in one of the site's subnets (or computers that do not belong to any site if - this is the default site), as well as users connecting to these computers. Note that Server - objects associated with the Site should be located in the Site. + 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 have control over an existing GPO (or the ability to create new ones), 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{' '} - - OUned.py - {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + From Linux, see{' '} - the article associated to the OUned.py tool - - . + href='https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites'> + the Site Unseen article + {' '} + for site-specific gPLink attack requirements and implementation details. - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) in that - GPO, and then link the GPO to the target Site through its gPLink attribute. To do so, you can - use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -776,7 +764,7 @@ const LinuxAbuse: FC = ( } - Now you can link the modified GPO to the Site object, through the 'link' command. + Then link the modified GPO to the site object with the 'link' command. { @@ -785,8 +773,8 @@ const LinuxAbuse: FC = ( - Be mindful of the number of users and computers that are in the given site as they all will - attempt to fetch and apply the malicious GPO. + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. ); 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 e381a4ee36d8..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,7 @@ const References: FC = () => { https://github.com/CravateRouge/bloodyAD +
= - In such a situation, it may still be possible to exploit GenericAll permissions on a domain - object. Indeed, with GenericAll permissions over a domain object, it is possible to modify its - gPLink attribute. This may be abused to apply a malicious Group Policy Object (GPO) to all of - the domain's user and computer objects (including the ones located in nested OUs). This can be - exploited to make said child objects execute arbitrary commands e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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{' '} = - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + 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. - 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. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -615,22 +613,21 @@ const WindowsAbuse: FC = return ( <> - With full control over a GPO, you may make modifications to that GPO in order to inject - malicious configurations into it. You could for instance add a Scheduled Task that will then be - executed by all of the computers and/or users to which the GPO applies, thus compromising them. - Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing - to precisely target a specific object. GPOs are applied 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 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, the native Group Policy Management Console (GPMC) may be - used to edit GPOs. On a non-domain joined Windows Machine, the{' '} + 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 can be used. + tool. ); @@ -726,26 +723,24 @@ const WindowsAbuse: FC = - In such a situation, it may still be possible to exploit GenericAll permissions on an OU. - Indeed, with GenericAll permissions over an OU, it is possible to modify its gPLink attribute. - This may be abused to apply a malicious Group Policy Object (GPO) to all of the OU's user and - computer objects (including the ones located in nested OUs). This can be exploited to make said - child objects execute arbitrary commands e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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{' '} = - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target OU object through its gPLink attribute. + 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. - 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. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -854,46 +849,46 @@ const WindowsAbuse: FC = return ( <> - GenericAll permissions over a Site object allow modifying the gPLink attribute of the site. The - ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious Group - Policy Object (GPO) to all of the objects associated with the site. This can be exploited to - make said objects execute arbitrary commands e.g. through an immediate scheduled task, thus - compromising them. In the case of a site, the affected objects are the computers that have an IP - address included in one of the site's subnets (or computers that do not belong to any site if - this is the default site), as well as users connecting to these computers. Note that Server - objects associated with the Site should be located in the Site. + 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 have control over an existing GPO (or the ability to create new ones), 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 site-specific requirements and + implementation details, see{' '} - this article + href='https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites'> + the Site Unseen article . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) in that - GPO, and then link the GPO to the target Site through its gPLink attribute. + 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. - Be mindful of the number of users and computers that are in the given site as they all will - attempt to fetch and apply the malicious GPO. + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. ); 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 40e455d6c54c..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,13 +184,12 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - With GenericWrite over a GPO, you may make modifications to that GPO in order to inject - malicious configurations into it. You could for instance add a Scheduled Task that will then be - executed by all of the computers and/or users to which the GPO applies, thus compromising them. - Note that some configurations (such as Scheduled Tasks) implement item-level targeting, allowing - to precisely target a specific object. GPOs are applied 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 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. @@ -201,10 +200,10 @@ const LinuxAbuse: FC = ({ targetType }) => { href='https://github.com/synacktiv/GroupPolicyBackdoor'> GroupPolicyBackdoor.py {' '} - tool can be used to perform the attack from a Linux machine. First, define a module file that - describes the configuration to inject. The following one defines a computer configuration, with - an immediate Scheduled Task adding a domain user as local administrator. A filter is defined, so - that it only applies to a specific target. + 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. @@ -222,8 +221,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - Place the described configuration into the Scheduled_task_add.ini file, and inject it into the - target GPO with the 'inject' command. + Save this configuration as Scheduled_task_add.ini, then inject it into the target GPO with the + 'inject' command. { @@ -236,7 +235,7 @@ const LinuxAbuse: FC = ({ targetType }) => { 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 @@ -252,47 +251,44 @@ 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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 . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target OU through its gPLink attribute. To do so, - you can use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -300,7 +296,7 @@ const LinuxAbuse: FC = ({ targetType }) => { } - You can then link the modified GPO to the OU, through the 'link' command. + Then link the modified GPO to the OU with the 'link' command. { @@ -309,8 +305,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - 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. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -318,48 +314,44 @@ 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 e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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 . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. - To do so, you can use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -367,7 +359,7 @@ const LinuxAbuse: FC = ({ targetType }) => { } - You can then link the modified GPO to the domain, through the 'link' command. + Then link the modified GPO to the domain with the 'link' command. { @@ -376,8 +368,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - 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. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -437,51 +429,46 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - GenericWrite permissions over a Site object allow modifying the gPLink attribute of the site. - The ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious - Group Policy Object (GPO) to all of the objects associated with the site. This can be exploited - to make said objects execute arbitrary commands e.g. through an immediate scheduled task, thus - compromising them. In the case of a site, the affected objects are the computers that have an IP - address included in one of the site's subnets (or computers that do not belong to any site if - this is the default site), as well as users connecting to these computers. Note that Server - objects associated with the Site should be located in the Site. + 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 have control over an existing GPO (or the ability to create new ones), 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{' '} - - OUned.py - {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + From Linux, see{' '} - the article associated to the OUned.py tool - - . + href='https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites'> + the Site Unseen article + {' '} + for site-specific gPLink attack requirements and implementation details. - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) in that - GPO, and then link the GPO to the target Site through its gPLink attribute. To do so, you can - use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -489,7 +476,7 @@ const LinuxAbuse: FC = ({ targetType }) => { } - Now you can link the modified GPO to the Site object, through the 'link' command. + Then link the modified GPO to the site object with the 'link' command. { @@ -498,8 +485,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - Be mindful of the number of users and computers that are in the given site as they all will - attempt to fetch and apply the malicious GPO. + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. ); diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx index 1cc84b09e770..a007c36478f1 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/GenericWrite/WindowsAbuse.tsx @@ -142,22 +142,21 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t return ( <> - With GenericWrite permissions over a GPO, you may make modifications to that GPO in order to - inject malicious configurations into it. You could for instance add a Scheduled Task that will - then be executed by all of the computers and/or users to which the GPO applies, thus - compromising them. Note that some configurations (such as Scheduled Tasks) implement item-level - targeting, allowing to precisely target a specific object. GPOs are applied 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 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, the native Group Policy Management Console (GPMC) may be - used to edit GPOs. On a non-domain joined Windows Machine, the{' '} + 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 can be used. + tool. This edge can be a false positive in rare scenarios. If you have GenericWrite on the GPO with @@ -278,25 +277,23 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t return ( <> - With GenericWrite permissions over an OU, it is possible to modify its gPLink attribute. This - may be abused to apply a malicious Group Policy Object (GPO) to all of the OU's user and - computer objects (including the ones located in nested OUs). This can be exploited to make said - child objects execute arbitrary commands e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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 - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target OU object through its gPLink attribute. + 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. - 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. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -322,25 +319,23 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t return ( <> - With GenericWrite permission over a domain object, it is possible to modify its gPLink - attribute. This may be abused to apply a malicious Group Policy Object (GPO) to all of the - domain's user and computer objects (including the ones located in nested OUs). This can be - exploited to make said child objects execute arbitrary commands e.g. 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. - If you do not have control over an existing GPO (or the ability to create new ones), 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 - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + 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. - 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. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -418,46 +413,46 @@ const WindowsAbuse: FC = ({ sourceName, sourceType, targetName, t return ( <> - GenericWrite permissions over a Site object allow modifying the gPLink attribute of the site. - The ability to alter the gPLink attribute of a site may allow an attacker to apply a malicious - Group Policy Object (GPO) to all of the objects associated with the site. This can be exploited - to make said objects execute arbitrary commands e.g. through an immediate scheduled task, thus - compromising them. In the case of a site, the affected objects are the computers that have an IP - address included in one of the site's subnets (or computers that do not belong to any site if - this is the default site), as well as users connecting to these computers. Note that Server - objects associated with the Site should be located in the Site. + 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 have control over an existing GPO (or the ability to create new ones), 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 site-specific requirements and + implementation details, see{' '} - this article + href='https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites'> + the Site Unseen article . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) in that - GPO, and then link the GPO to the target Site through its gPLink attribute. + 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. - Be mindful of the number of users and computers that are in the given site as they all will - attempt to fetch and apply the malicious GPO. + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. ); 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 f764186d3d30..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,21 +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 of an object may allow an attacker to apply a malicious Group - Policy Object (GPO) to all child user and computer objects. This can be exploited to make said child - objects execute arbitrary commands through e.g. 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. - For domain and OU objects, child user/computer objects are the ones belonging to the domain/OU - (including the ones located in nested OUs). In the case of a site, the affected objects are the - computers that have an IP address included in one of the site's subnets (or computers that do not belong - to any site if this is the default site), as well as users connecting to these computers. Note that - Server objects associated with the Site should be located in the Site. + 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/WriteGPLink/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/WriteGPLink/LinuxAbuse.tsx index bddd6f7a3776..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 @@ -25,39 +25,37 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - If you do not have control over an existing GPO (or the ability to create new ones), 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 . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. - To do so, you can use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -65,7 +63,7 @@ const LinuxAbuse: FC = ({ targetType }) => { } - You can then link the modified GPO to the domain, through the 'link' command. + Then link the modified GPO to the domain with the 'link' command. { @@ -74,8 +72,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - 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. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -83,39 +81,37 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - If you do not have control over an existing GPO (or the ability to create new ones), 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 . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target OU through its gPLink attribute. To do so, - you can use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -123,7 +119,7 @@ const LinuxAbuse: FC = ({ targetType }) => { } - You can then link the modified GPO to the OU, through the 'link' command. + Then link the modified GPO to the OU with the 'link' command. { @@ -132,8 +128,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - 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. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -141,40 +137,34 @@ const LinuxAbuse: FC = ({ targetType }) => { return ( <> - If you do not have control over an existing GPO (or the ability to create new ones), 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{' '} - - OUned.py - {' '} - tool. For a detailed outline of exploit requirements and implementation, you can refer to{' '} + From Linux, see{' '} - the article associated to the OUned.py tool - - . + href='https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites'> + the Site Unseen article + {' '} + for site-specific gPLink attack requirements and implementation details. - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) in that - GPO, and then link the GPO to the target Site through its gPLink attribute. To do so, you can - use the{' '} + 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 {' '} - tool. You may for instance first inject the malicious configuration with the 'inject' command. + for this. For example, first inject the malicious configuration with the 'inject' command. { @@ -182,7 +172,7 @@ const LinuxAbuse: FC = ({ targetType }) => { } - Now you can link the modified GPO to the Site object, through the 'link' command. + Then link the modified GPO to the site object with the 'link' command. { @@ -191,8 +181,8 @@ const LinuxAbuse: FC = ({ targetType }) => { - Be mindful of the number of users and computers that are in the given site as they all will - attempt to fetch and apply the malicious GPO. + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. ); 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 ece0402a1211..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,7 @@ 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 +
= ({ targetType }) => { return ( <> - If you do not have control over an existing GPO (or the ability to create new ones), 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{' '} = ({ targetType }) => { - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target domain object through its gPLink attribute. + 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. - 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. + Consider how many users and computers the target domain contains; each affected object will + attempt to retrieve and apply the malicious GPO. ); @@ -61,17 +60,16 @@ const WindowsAbuse: FC = ({ targetType }) => { return ( <> - If you do not have control over an existing GPO (or the ability to create new ones), 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{' '} = ({ targetType }) => { - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) into a - controlled GPO, and then link the GPO to the target OU object through its gPLink attribute. + 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. - 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. + Consider how many users and computers the target OU contains; each affected object will attempt + to retrieve and apply the malicious GPO. ); @@ -97,35 +95,34 @@ const WindowsAbuse: FC = ({ targetType }) => { return ( <> - If you do not have control over an existing GPO (or the ability to create new ones), 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 site-specific requirements and + implementation details, see{' '} - this article + href='https://www.synacktiv.com/publications/site-unseen-enumerating-and-attacking-active-directory-sites'> + the Site Unseen article . - If you have control over an existing GPO (or the ability to create new ones), the attack is - simpler. You can inject a malicious configuration (e.g. an immediate scheduled task) in that - GPO, and then link the GPO to the target Site through its gPLink attribute. + 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. - Be mindful of the number of users and computers that are in the given site as they all will - attempt to fetch and apply the malicious GPO. + Consider how many computers and users the target site affects; each affected object will attempt + to retrieve and apply the malicious GPO. ); 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 3d1219ac93e4..c8b78a5a9b68 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx @@ -56,7 +56,7 @@ export const DomainMap = { issuancepolicies: { displayText: 'IssuancePolicies', kind: ActiveDirectoryNodeKind.IssuancePolicy }, sites: { displayText: 'Sites', kind: ActiveDirectoryNodeKind.Site }, siteservers: { displayText: 'SiteServers', kind: ActiveDirectoryNodeKind.SiteServer }, - sitesubnets: { displayText: 'SiteSubnet', kind: ActiveDirectoryNodeKind.SiteSubnet }, + 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 9c3b9751ebd3..7ab1a126a3c3 100644 --- a/packages/javascript/bh-shared-ui/src/views/Explore/BasicObjectInfoFields.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/BasicObjectInfoFields.tsx @@ -59,7 +59,9 @@ const RelatedKindField = ( onSourceNodeSelected({ objectid: id, type: relatedKind, name: name || displayValue || '' })} + onClick={() => + onSourceNodeSelected({ objectid: id, type: relatedKind, name: name || displayValue || '' }) + } style={{ cursor: 'pointer' }} overflow='hidden' textOverflow='ellipsis' @@ -102,7 +104,7 @@ export const BasicObjectInfoFields: React.FC = (prop {props.serverreferencecomputer && RelatedKindField( props.handleSourceNodeSelected, - 'Server Reference Computer:', + 'Referenced Computer:', ActiveDirectoryNodeKind.Computer, props.serverreferencecomputer, props.serverreferencecomputername, From 067116b155105d4787c18ed663a13c85e91adb88 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Wed, 24 Jun 2026 14:26:29 +0200 Subject: [PATCH 12/15] fix unused parameters --- packages/javascript/bh-shared-ui/src/utils/content.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/javascript/bh-shared-ui/src/utils/content.ts b/packages/javascript/bh-shared-ui/src/utils/content.ts index d09904c6f38f..223d88626cb8 100644 --- a/packages/javascript/bh-shared-ui/src/utils/content.ts +++ b/packages/javascript/bh-shared-ui/src/utils/content.ts @@ -1078,8 +1078,8 @@ export const allSections: Partial EntityInfo queryType: 'site-linked_sitesubnets', }, ], - [ActiveDirectoryNodeKind.SiteServer]: (id: string) => [], - [ActiveDirectoryNodeKind.SiteSubnet]: (id: string) => [], + [ActiveDirectoryNodeKind.SiteServer]: () => [], + [ActiveDirectoryNodeKind.SiteSubnet]: () => [], [ActiveDirectoryNodeKind.User]: (id: string) => [ { id, From 4ff263429ae25330d641e8d80f3d0fef1a6d17b0 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Wed, 24 Jun 2026 14:55:22 +0200 Subject: [PATCH 13/15] node colors --- .../src/database/migration/extensions/ad_graph_schema.sql | 6 +++--- packages/go/schemagen/generator/sql.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql index 052a40925c2e..906a16805707 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -262,9 +262,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', '#bababdff'); - PERFORM genscript_upsert_schema_node_kind(extension_id, 'SiteServer', 'SiteServer', '', true, 'map-marker', '#dcdce6ff'); - PERFORM genscript_upsert_schema_node_kind(extension_id, 'SiteSubnet', 'SiteSubnet', '', true, 'map', '#fdfdfdff'); + 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'); PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'Owns', '', true); PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'GenericAll', '', true); diff --git a/packages/go/schemagen/generator/sql.go b/packages/go/schemagen/generator/sql.go index 33eea5d20de2..c7b9c00747db 100644 --- a/packages/go/schemagen/generator/sql.go +++ b/packages/go/schemagen/generator/sql.go @@ -78,15 +78,15 @@ var nodeIcons = map[string]nodeIcon{ }, "Site": { Icon: "map-signs", - Color: "#bababdff", + Color: "#2DD4BF", }, "SiteServer": { Icon: "map-marker", - Color: "#dcdce6ff", + Color: "#60A5FA", }, "SiteSubnet": { Icon: "map", - Color: "#fdfdfdff", + Color: "#F59E0B", }, "OU": { Icon: "sitemap", From ad761e19775f4a2e69621ea896f0563e8c577ed5 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Wed, 24 Jun 2026 15:01:54 +0200 Subject: [PATCH 14/15] add ServerIs to edge filter --- .../views/Explore/ExploreSearch/EdgeFilter/edgeCategories.tsx | 1 + 1 file changed, 1 insertion(+) 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, ], }, { From 7072f6c0f167d46f90c82c56b51b2dbc23f12ff0 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Wed, 24 Jun 2026 15:55:19 +0200 Subject: [PATCH 15/15] fix schema --- cmd/api/src/database/migration/extensions/ad_graph_schema.sql | 3 +++ 1 file changed, 3 insertions(+) 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 2ae261095202..5f05db4fc09a 100644 --- a/cmd/api/src/database/migration/extensions/ad_graph_schema.sql +++ b/cmd/api/src/database/migration/extensions/ad_graph_schema.sql @@ -300,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);