Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8793a76
Entra Domain Services hybrid edges
martinsohn Jul 7, 2026
d098e0e
Remove SyncedToEntraDSGroup from Pathfinding filter
martinsohn Jul 8, 2026
2d5ccec
HelpText newlines
martinsohn Jul 8, 2026
f4019ce
Fix Entra Domain Services schema metadata
martinsohn Jul 22, 2026
80dbda0
Merge remote-tracking branch 'origin/main' into entra-domain-services
martinsohn Aug 3, 2026
a231bb5
chore: ignore local Claude settings
martinsohn Aug 3, 2026
adc9626
feat: add Entra Domain Services graph model
martinsohn Aug 3, 2026
40baa4e
feat: add Entra Domain Services UI support
martinsohn Aug 3, 2026
f89d804
Merge remote-tracking branch 'origin/main' into microsoft-entra-domai…
martinsohn Aug 3, 2026
05bfe33
Rename DomainService nodes-edges to EntraDS
martinsohn Aug 5, 2026
e31a7ea
Merge remote-tracking branch 'origin/main' into microsoft-entra-domai…
martinsohn Aug 10, 2026
2b0b009
fix(schemagen): support portable schema overlays
martinsohn Aug 10, 2026
3f22e7d
feat: add Entra DS graph model and analysis
martinsohn Aug 10, 2026
2fceddc
feat(ui): add Entra DS searches
martinsohn Aug 10, 2026
e072469
fix(ui): avoid stale edge composition queries
martinsohn Aug 10, 2026
f080689
Merge remote-tracking branch 'origin/main' into microsoft-entra-domai…
martinsohn Aug 10, 2026
3700ff2
feat(ui): add Entra DS help text
martinsohn Aug 10, 2026
50e72d0
Refine AZManageEntraDS composition
martinsohn Aug 10, 2026
f31591a
Merge remote-tracking branch 'origin/main' into microsoft-entra-domai…
martinsohn Aug 10, 2026
ca4ddff
fix: tolerate invalid Entra DS sync prerequisites
martinsohn Aug 11, 2026
9b02b6d
fix: harden Entra DS hybrid analysis
martinsohn Aug 11, 2026
b4614a5
docs: clarify Entra DS identity correlation
martinsohn Aug 11, 2026
dfd59d0
docs: complete Entra DS help and queries
martinsohn Aug 11, 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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# Codex
.codex

# Claude
.claude/settings.local.json

# Augment
.augment

Expand Down Expand Up @@ -263,6 +266,9 @@ fabric.properties
# Test binary, built with `go test -c`
*.test

# Repository-local Go build cache overrides
.gocache/

# Golang workspace sum files
go.work.sum

Expand Down
6 changes: 6 additions & 0 deletions cmd/api/src/api/v2/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const (
entityTypeServicePrincipals = "service-principals"
entityTypeRoles = "roles"
entityTypeFunctionApps = "function-apps"
entityTypeDomainServices = "domain-services"
entityTypeFederatedIdentityCredentials = "federated-identity-credentials"
)

Expand Down Expand Up @@ -444,6 +445,8 @@ func GetAZEntityInformation(ctx context.Context, db database.Database, graphDb g
return azure.RoleEntityDetails(ctx, graphDb, primaryDisplayKinds, objectID, hydrateCounts)
case entityTypeFunctionApps:
return azure.FunctionAppEntityDetails(ctx, graphDb, primaryDisplayKinds, objectID, hydrateCounts)
case entityTypeDomainServices:
return azure.DomainServiceEntityDetails(ctx, graphDb, primaryDisplayKinds, objectID, hydrateCounts)
case entityTypeFederatedIdentityCredentials:
return azure.FederatedIdentityCredentialEntityDetails(ctx, graphDb, primaryDisplayKinds, objectID, hydrateCounts)
default:
Expand Down Expand Up @@ -555,6 +558,9 @@ func azEntityParamToKind(entityType string) (graph.Kind, error) {
case entityTypeFunctionApps:
return azure_schema.FunctionApp, nil

case entityTypeDomainServices:
return azure_schema.EntraDS, nil

case entityTypeFederatedIdentityCredentials:
return azure_schema.FederatedIdentityCredential, nil

Expand Down
30 changes: 30 additions & 0 deletions cmd/api/src/api/v2/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,36 @@ func TestResources_GetAZEntityInformation(t *testing.T) {
err: nil,
},
},
{
name: "Error: entityTypeDomainServices",
args: args{
entityType: "domain-services",
},
setupMocks: func(t *testing.T, mocks *mock) {
t.Helper()
mocks.mockDatabase.EXPECT().GetPrimaryDisplayKinds(gomock.Any())
mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error"))
},
want: want{
res: nil,
err: errors.New("error"),
},
},
{
name: "Success: entityTypeDomainServices",
args: args{
entityType: "domain-services",
},
setupMocks: func(t *testing.T, mocks *mock) {
t.Helper()
mocks.mockDatabase.EXPECT().GetPrimaryDisplayKinds(gomock.Any())
mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil)
},
want: want{
res: azure.DomainServiceDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0},
err: nil,
},
},
{
name: "Error: unknown azure entity",
args: args{
Expand Down
3 changes: 2 additions & 1 deletion cmd/api/src/api/v2/edge.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/specterops/bloodhound/cmd/api/src/model"
"github.com/specterops/bloodhound/packages/go/analysis"
"github.com/specterops/bloodhound/packages/go/analysis/edgecomposition"
"github.com/specterops/bloodhound/packages/go/ein"

"github.com/specterops/bloodhound/cmd/api/src/api"
Expand Down Expand Up @@ -99,7 +100,7 @@ func (s *Resources) GetEdgeComposition(response http.ResponseWriter, request *ht
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("Invalid value for endID: %s", targetNode[0]), request), response)
} else if edge, err := analysis.FetchEdgeByStartAndEnd(request.Context(), s.Graph, graph.ID(startID), graph.ID(endID), kind); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("Could not find edge matching criteria: %v", err), request), response)
} else if pathSet, err := ad.GetEdgeCompositionPath(request.Context(), s.Graph, edge); err != nil {
} else if pathSet, err := edgecomposition.GetEdgeCompositionPath(request.Context(), s.Graph, edge); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("Error getting composition for edge: %v", err), request), response)
} else if primaryDisplayKinds, err := s.DB.GetPrimaryDisplayKinds(request.Context()); err != nil {
api.HandleDatabaseError(request, response, err)
Expand Down
19 changes: 19 additions & 0 deletions cmd/api/src/database/migration/extensions/az_graph_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ BEGIN
PERFORM genscript_upsert_kind('AZRole');
PERFORM genscript_upsert_kind('AZDevice');
PERFORM genscript_upsert_kind('AZFunctionApp');
PERFORM genscript_upsert_kind('AZEntraDS');
PERFORM genscript_upsert_kind('AZGroup');
PERFORM genscript_upsert_kind('AZKeyVault');
PERFORM genscript_upsert_kind('AZManagementGroup');
Expand All @@ -181,6 +182,8 @@ BEGIN
PERFORM genscript_upsert_kind('AZAvereContributor');
PERFORM genscript_upsert_kind('AZContains');
PERFORM genscript_upsert_kind('AZContributor');
PERFORM genscript_upsert_kind('AZEntraDSContributor');
PERFORM genscript_upsert_kind('AZManageEntraDS');
PERFORM genscript_upsert_kind('AZGetCertificates');
PERFORM genscript_upsert_kind('AZGetKeys');
PERFORM genscript_upsert_kind('AZGetSecrets');
Expand Down Expand Up @@ -225,6 +228,12 @@ BEGIN
PERFORM genscript_upsert_kind('AZMGGrantAppRoles');
PERFORM genscript_upsert_kind('AZMGGrantRole');
PERFORM genscript_upsert_kind('SyncedToEntraUser');
PERFORM genscript_upsert_kind('SyncedToEntraDSUser');
PERFORM genscript_upsert_kind('SyncedToEntraDSGroup');
PERFORM genscript_upsert_kind('AddEntraDSGroupMember');
PERFORM genscript_upsert_kind('EntraDSFor');
PERFORM genscript_upsert_kind('ManageEntraDSSync');
PERFORM genscript_upsert_kind('ManageEntraDSSyncFilter');
PERFORM genscript_upsert_kind('AZRoleEligible');
PERFORM genscript_upsert_kind('AZRoleApprover');
PERFORM genscript_upsert_kind('AZAuthenticatesTo');
Expand All @@ -235,6 +244,7 @@ BEGIN
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZRole', 'AZRole', '', true, 'clipboard-list', '#ED8537');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZDevice', 'AZDevice', '', true, 'desktop', '#B18FCF');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZFunctionApp', 'AZFunctionApp', '', true, 'bolt', '#F4BA44');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZEntraDS', 'AZEntraDS', '', true, 'server', '#6D83F2');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZGroup', 'AZGroup', '', true, 'users', '#F57C9B');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZKeyVault', 'AZKeyVault', '', true, 'lock', '#ED658C');
PERFORM genscript_upsert_schema_node_kind(extension_id, 'AZManagementGroup', 'AZManagementGroup', '', true, 'sitemap', '#BD93D8');
Expand All @@ -257,6 +267,7 @@ BEGIN
PERFORM genscript_upsert_custom_node_kind('AZRole', '{"icon": {"name": "clipboard-list", "type": "font-awesome", "color": "#ED8537"}}');
PERFORM genscript_upsert_custom_node_kind('AZDevice', '{"icon": {"name": "desktop", "type": "font-awesome", "color": "#B18FCF"}}');
PERFORM genscript_upsert_custom_node_kind('AZFunctionApp', '{"icon": {"name": "bolt", "type": "font-awesome", "color": "#F4BA44"}}');
PERFORM genscript_upsert_custom_node_kind('AZEntraDS', '{"icon": {"name": "server", "type": "font-awesome", "color": "#6D83F2"}}');
PERFORM genscript_upsert_custom_node_kind('AZGroup', '{"icon": {"name": "users", "type": "font-awesome", "color": "#F57C9B"}}');
PERFORM genscript_upsert_custom_node_kind('AZKeyVault', '{"icon": {"name": "lock", "type": "font-awesome", "color": "#ED658C"}}');
PERFORM genscript_upsert_custom_node_kind('AZManagementGroup', '{"icon": {"name": "sitemap", "type": "font-awesome", "color": "#BD93D8"}}');
Expand All @@ -276,6 +287,8 @@ BEGIN
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZAvereContributor', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZContains', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZContributor', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZEntraDSContributor', '', false);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZManageEntraDS', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZGetCertificates', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZGetKeys', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZGetSecrets', '', true);
Expand Down Expand Up @@ -320,6 +333,12 @@ BEGIN
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZMGGrantAppRoles', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZMGGrantRole', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'SyncedToEntraUser', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'SyncedToEntraDSUser', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'SyncedToEntraDSGroup', '', false);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AddEntraDSGroupMember', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'EntraDSFor', '', false);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'ManageEntraDSSync', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'ManageEntraDSSyncFilter', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZRoleEligible', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZRoleApprover', '', true);
PERFORM genscript_upsert_schema_relationship_kind(extension_id, 'AZAuthenticatesTo', '', true);
Expand Down
31 changes: 31 additions & 0 deletions cmd/api/src/services/graphify/azure_convertors.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ func getKindConverter(kind enums.Kind) func(json.RawMessage, *ConvertedAzureData
return convertAzureFunctionApp
case enums.KindAZFunctionAppRoleAssignment:
return convertAzureFunctionAppRoleAssignment
case enums.Kind("AZEntraDS"):
return convertAzureDomainService
case enums.Kind("AZEntraDSRoleAssignment"):
return convertAzureDomainServiceRoleAssignment
case enums.KindAZGroup:
return convertAzureGroup
case enums.KindAZGroupMember:
Expand Down Expand Up @@ -332,6 +336,33 @@ func convertAzureFunctionAppRoleAssignment(raw json.RawMessage, converted *Conve
}
}

func convertAzureDomainService(raw json.RawMessage, converted *ConvertedAzureData, ingestTime time.Time) {
var data ein.AzureDomainService
if err := json.Unmarshal(raw, &data); err != nil {
slog.Error(
SerialError,
slog.String("type", "domain service"),
attr.Error(err),
)
} else {
converted.NodeProps = append(converted.NodeProps, ein.ConvertAzureDomainServiceToNode(data, ingestTime))
converted.RelProps = append(converted.RelProps, ein.ConvertAzureDomainServiceToRels(data)...)
}
}

func convertAzureDomainServiceRoleAssignment(raw json.RawMessage, converted *ConvertedAzureData, ingestTime time.Time) {
var data models.AzureRoleAssignments
if err := json.Unmarshal(raw, &data); err != nil {
slog.Error(
SerialError,
slog.String("type", "domain service role assignments"),
attr.Error(err),
)
} else {
converted.RelProps = append(converted.RelProps, ein.ConvertAzureDomainServiceRoleAssignmentToRels(data)...)
}
}

func convertAzureGroup(raw json.RawMessage, converted *ConvertedAzureData, ingestTime time.Time) {
var data models.Group
if err := json.Unmarshal(raw, &data); err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/ui/src/ducks/graph/graphutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const ICONS: { [id in GraphNodeTypes]: string } = {
[GraphNodeTypes.AZRole]: 'fa-window-restore',
[GraphNodeTypes.AZDevice]: 'fa-desktop',
[GraphNodeTypes.AZFunctionApp]: 'fa-bolt',
[GraphNodeTypes.AZEntraDS]: 'fa-server',
[GraphNodeTypes.AZGroup]: 'fa-users',
[GraphNodeTypes.AZKeyVault]: 'fa-lock',
[GraphNodeTypes.AZManagementGroup]: 'fa-cube',
Expand Down
1 change: 1 addition & 0 deletions cmd/ui/src/ducks/graph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum GraphNodeTypes {
AZRole = 'AZRole',
AZDevice = 'AZDevice',
AZFunctionApp = 'AZFunctionApp',
AZEntraDS = 'AZEntraDS',
AZGroup = 'AZGroup',
AZKeyVault = 'AZKeyVault',
AZManagementGroup = 'AZManagementGroup',
Expand Down
1 change: 1 addition & 0 deletions packages/csharp/graphschema/PropertyNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public static class PropertyNames {
public static readonly string CertTemplateOID = "certtemplateoid";
public static readonly string GroupLinkID = "grouplinkid";
public static readonly string ObjectGUID = "objectguid";
public static readonly string AADObjectID = "aadobjectid";
public static readonly string ExpirePasswordsOnSmartCardOnlyAccounts = "expirepasswordsonsmartcardonlyaccounts";
public static readonly string MachineAccountQuota = "machineaccountquota";
public static readonly string SupportedKerberosEncryptionTypes = "supportedencryptiontypes";
Expand Down
8 changes: 8 additions & 0 deletions packages/cue/bh/ad/ad.cue
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,13 @@ ObjectGUID: types.#StringEnum & {
representation: "objectguid"
}

AADObjectID: types.#StringEnum & {
symbol: "AADObjectID"
schema: "ad"
name: "Microsoft Entra Object ID"
representation: "aadobjectid"
}

ExpirePasswordsOnSmartCardOnlyAccounts: types.#StringEnum & {
symbol: "ExpirePasswordsOnSmartCardOnlyAccounts"
schema: "ad"
Expand Down Expand Up @@ -1120,6 +1127,7 @@ Properties: [
CertTemplateOID,
GroupLinkID,
ObjectGUID,
AADObjectID,
ExpirePasswordsOnSmartCardOnlyAccounts,
MachineAccountQuota,
SupportedKerberosEncryptionTypes,
Expand Down
Loading
Loading