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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions cmd/api/src/analysis/ad/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
31 changes: 31 additions & 0 deletions cmd/api/src/api/v2/ad_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down Expand Up @@ -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)
}
12 changes: 12 additions & 0 deletions cmd/api/src/api/v2/ad_related_entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
}
55 changes: 55 additions & 0 deletions cmd/api/src/database/migration/migrations/v8.4.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 6 additions & 0 deletions cmd/api/src/model/adquality.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
14 changes: 13 additions & 1 deletion cmd/api/src/model/ingest/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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"
)

Expand All @@ -125,6 +134,9 @@ func AllIngestDataTypes() []DataType {
DataTypeCertTemplate,
DataTypeAzure,
DataTypeIssuancePolicy,
DataTypeSite,
DataTypeSiteServer,
DataTypeSiteSubnet,
}
}

Expand Down
34 changes: 34 additions & 0 deletions cmd/api/src/services/graphify/convertors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
3 changes: 3 additions & 0 deletions cmd/api/src/services/graphify/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
3 changes: 3 additions & 0 deletions cmd/ui/src/ducks/graph/graphutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
3 changes: 3 additions & 0 deletions cmd/ui/src/ducks/graph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export enum GraphNodeTypes {
NTAuthStore = 'NTAuthStore',
CertTemplate = 'CertTemplate',
IssuancePolicy = 'IssuancePolicy',
Site = 'Site',
SiteServer = 'SiteServer',
SiteSubnet = 'SiteSubnet',
}

export interface GraphNodeData {
Expand Down
18 changes: 18 additions & 0 deletions packages/cue/bh/ad/ad.cue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1278,6 +1293,9 @@ NodeKinds: [
NTAuthStore,
CertTemplate,
IssuancePolicy,
Site,
SiteServer,
SiteSubnet
]

Owns: types.#Kind & {
Expand Down
4 changes: 4 additions & 0 deletions packages/go/analysis/ad/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading