Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6fe1f93
feat: Add visualisation of Active Directory site data
q-roland Nov 3, 2025
892c148
feat: update AD site ingest from SharpHoundCommon
JonasBK Jun 15, 2026
7b8c31e
Merge branch 'main' into pr-2031-ad-sites
JonasBK Jun 15, 2026
6880962
fix: align AD site support with current main BED-8138
JonasBK Jun 15, 2026
52075aa
Merge branch 'main' into pr-2031-ad-sites
JonasBK Jun 15, 2026
531be7e
fix entity panels
JonasBK Jun 15, 2026
8185430
pz rule for dc site servers
JonasBK Jun 15, 2026
52aa6a1
make ServerIs traversable
JonasBK Jun 15, 2026
5fa500f
fix site nodes entity panels
JonasBK Jun 15, 2026
acc2bbd
Add cypher queries
JonasBK Jun 16, 2026
8431191
better colors
JonasBK Jun 16, 2026
9598895
add ServerIs refs in entity panel
JonasBK Jun 16, 2026
5bee575
improve edge texts
JonasBK Jun 23, 2026
067116b
fix unused parameters
JonasBK Jun 24, 2026
4ff2634
node colors
JonasBK Jun 24, 2026
ad761e1
add ServerIs to edge filter
JonasBK Jun 24, 2026
e2edb30
Merge branch 'main' into pr-2031-ad-sites
JonasBK Jun 24, 2026
7072f6c
fix schema
JonasBK Jun 24, 2026
a4630a4
fix Serverreference
JonasBK Aug 14, 2026
6ca4eaf
add siteobject and objectclass props
JonasBK Aug 14, 2026
82697a4
getGPOLinksWithoutSite
JonasBK Aug 14, 2026
691daa3
Shared container delegate
JonasBK Aug 14, 2026
9bfacb2
fix ServerIs linking
JonasBK Aug 14, 2026
010b7f0
ownership-safe migrations
JonasBK Aug 14, 2026
a38b365
direct-Site graph assertions in test
JonasBK Aug 14, 2026
e808199
Merge branch 'main' into pr-2031-ad-sites
JonasBK Aug 14, 2026
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