Skip to content
Closed
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
35 changes: 32 additions & 3 deletions cmd/api/src/analysis/azure/azure_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func TestAzureEntityGroupMembership(t *testing.T) {
if groupPaths, err := azureanalysis.FetchEntityGroupMembershipPaths(tx, harness.AZBaseHarness.User); err != nil {
t.Fatal(err)
} else {
assert.ElementsMatch(t, harness.AZBaseHarness.UserFirstDegreeGroups.IDs(), groupPaths.AllNodes().ContainingNodeKinds(azure.Group).IDs())
assert.ElementsMatch(t, harness.AZBaseHarness.UserFirstDegreeGroups.IDs(), groupPaths.AllNodes().ContainingNodeKinds(azure.Group, azure.Group365).IDs())
}
})
}
Expand Down Expand Up @@ -554,6 +554,31 @@ func TestGroupEntityDetails(t *testing.T) {
})
}

func TestGroup365EntityDetails(t *testing.T) {
testContext := integration.NewGraphTestContext(t, schema.DefaultGraphSchema())
testContext.ReadTransactionTestWithSetup(func(harness *integration.HarnessDetails) error {
harness.AZEntityPanelHarness.Setup(testContext)
return nil

}, func(harness integration.HarnessDetails, tx graph.Transaction) {

groupObjectID, err := harness.AZEntityPanelHarness.Group365.Properties.Get(common.ObjectID.String()).String()
require.Nil(t, err)

assert.NotEqual(t, "", groupObjectID)

group, err := azureanalysis.Group365EntityDetails(testContext.Graph.Database, groupObjectID, false)

require.Nil(t, err)
assert.Equal(t, harness.AZEntityPanelHarness.Group365.Properties.Get(common.ObjectID.String()).Any(), group.Properties[common.ObjectID.String()])
assert.Equal(t, 0, group.InboundObjectControl)

group, err = azureanalysis.Group365EntityDetails(testContext.Graph.Database, groupObjectID, true)
require.Nil(t, err)
assert.NotEqual(t, 0, group.InboundObjectControl)
})
}

func TestManagementGroupEntityDetails(t *testing.T) {
testContext := integration.NewGraphTestContext(t, schema.DefaultGraphSchema())
testContext.ReadTransactionTestWithSetup(func(harness *integration.HarnessDetails) error {
Expand Down Expand Up @@ -807,11 +832,13 @@ func TestFetchInboundEntityObjectControlPaths(t *testing.T) {
paths, err := azureanalysis.FetchInboundEntityObjectControlPaths(tx, harness.AZInboundControlHarness.ControlledAZUser)
require.Nil(t, err)
nodes := paths.AllNodes().IDs()
require.Equal(t, 8, len(nodes))
require.Equal(t, 10, len(nodes))
require.NotContains(t, nodes, harness.AZInboundControlHarness.AZAppA.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.ControlledAZUser.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupA.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupB.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365A.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365B.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalA.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalB.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZUserA.ID)
Expand All @@ -830,11 +857,13 @@ func TestFetchInboundEntityObjectControllers(t *testing.T) {
control, err := azureanalysis.FetchInboundEntityObjectControllers(tx, harness.AZInboundControlHarness.ControlledAZUser, 0, 0)
require.Nil(t, err)
nodes := control.IDs()
require.Equal(t, 7, len(nodes))
require.Equal(t, 9, len(nodes))
require.NotContains(t, nodes, harness.AZInboundControlHarness.ControlledAZUser.ID)
require.NotContains(t, nodes, harness.AZInboundControlHarness.AZAppA.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupA.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroupB.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365A.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZGroup365B.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalA.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZServicePrincipalB.ID)
require.Contains(t, nodes, harness.AZInboundControlHarness.AZUserA.ID)
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/src/analysis/azure/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func GraphStats(ctx context.Context, db graph.Database) (model.AzureDataQualityS
stat.Groups = int(count)
aggregation.Groups += int(count)

case azure.Group365:
stat.Groups365 = int(count)
aggregation.Groups365 += int(count)

case azure.App:
stat.Apps = int(count)
aggregation.Apps += int(count)
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/analysis/azure/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func TestAnalysisAzure_GraphStats(t *testing.T) {
assert.NotZero(t, agg.Tenants)
assert.NotZero(t, agg.Users)
assert.NotZero(t, agg.Groups)
assert.NotZero(t, agg.Groups365)
assert.NotZero(t, agg.Apps)
assert.NotZero(t, agg.ServicePrincipals)
assert.NotZero(t, agg.Devices)
Expand Down
6 changes: 6 additions & 0 deletions cmd/api/src/api/bloodhoundgraph/bloodhoundgraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ func (s *BloodHoundGraphNode) SetIcon(nType string) {
s.FontIcon = &BloodHoundGraphFontIcon{
Text: "fa-users",
}
case "AZGroup365":
s.FontIcon = &BloodHoundGraphFontIcon{
Text: "fa-users",
}
case "AZKeyVault":
s.FontIcon = &BloodHoundGraphFontIcon{
Text: "fa-lock",
Expand Down Expand Up @@ -319,6 +323,8 @@ func (s *BloodHoundGraphNode) SetBackground(nType string) {
s.BloodHoundGraphItem.Color = "#17E625"
case "Group":
s.BloodHoundGraphItem.Color = "#DBE617"
case "AZGroup365":
s.BloodHoundGraphItem.Color = "#34D2EB"
case "Computer":
s.BloodHoundGraphItem.Color = "#E67873"
case "Container":
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/src/api/v2/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
entityTypeBase = "az-base"
entityTypeUsers = "users"
entityTypeGroups = "groups"
entityTypeGroups365 = "groups365"
entityTypeTenants = "tenants"
entityTypeManagementGroups = "management-groups"
entityTypeSubscriptions = "subscriptions"
Expand Down Expand Up @@ -339,6 +340,9 @@ func GetAZEntityInformation(ctx context.Context, db graph.Database, entityType,
case entityTypeGroups:
return azure.GroupEntityDetails(db, objectID, hydrateCounts)

case entityTypeGroups365:
return azure.Group365EntityDetails(db, objectID, hydrateCounts)

case entityTypeTenants:
return azure.TenantEntityDetails(db, objectID, hydrateCounts)

Expand Down
47 changes: 47 additions & 0 deletions cmd/api/src/daemons/datapipe/azure_convertors.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,16 @@ func getKindConverter(kind enums.Kind) func(json.RawMessage, *ConvertedAzureData
return convertAzureFunctionAppRoleAssignment
case enums.KindAZGroup:
return convertAzureGroup
case enums.KindAZGroup365:
return convertAzureGroup365
case enums.KindAZGroupMember:
return convertAzureGroupMember
case enums.KindAZGroup365Member:
return convertAzureGroup365Member
case enums.KindAZGroupOwner:
return convertAzureGroupOwner
case enums.KindAZGroup365Owner:
return convertAzureGroup365Owner
case enums.KindAZKeyVault:
return convertAzureKeyVault
case enums.KindAZKeyVaultAccessPolicy:
Expand Down Expand Up @@ -282,6 +288,24 @@ func convertAzureGroup(raw json.RawMessage, converted *ConvertedAzureData) {
}
}

func convertAzureGroup365(raw json.RawMessage, converted *ConvertedAzureData) {

var data models.Group365

if err := json.Unmarshal(raw, &data); err != nil {

slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 36 group", err))

} else {

converted.NodeProps = append(converted.NodeProps, ein.ConvertAzureGroup365ToNode(data))

converted.RelProps = append(converted.RelProps, ein.ConvertAzureGroup365ToRel(data))

}

}

func convertAzureGroupMember(raw json.RawMessage, converted *ConvertedAzureData) {
var (
data models.GroupMembers
Expand All @@ -294,6 +318,18 @@ func convertAzureGroupMember(raw json.RawMessage, converted *ConvertedAzureData)
}
}

func convertAzureGroup365Member(raw json.RawMessage, converted *ConvertedAzureData) {
var (
data models.Group365Members
)

if err := json.Unmarshal(raw, &data); err != nil {
slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 365 group members", err))
} else {
converted.RelProps = append(converted.RelProps, ein.ConvertAzureGroup365MembersToRels(data)...)
}
}

func convertAzureGroupOwner(raw json.RawMessage, converted *ConvertedAzureData) {
var (
data models.GroupOwners
Expand All @@ -305,6 +341,17 @@ func convertAzureGroupOwner(raw json.RawMessage, converted *ConvertedAzureData)
}
}

func convertAzureGroup365Owner(raw json.RawMessage, converted *ConvertedAzureData) {
var (
data models.Group365Owners
)
if err := json.Unmarshal(raw, &data); err != nil {
slog.Error(fmt.Sprintf(SerialError, "azure Microsoft 365 group owners", err))
} else {
converted.RelProps = append(converted.RelProps, ein.ConvertAzureGroup365OwnerToRels(data)...)
}
}

func convertAzureKeyVault(raw json.RawMessage, converted *ConvertedAzureData) {
var data models.KeyVault
if err := json.Unmarshal(raw, &data); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions cmd/api/src/database/dataquality.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ WITH aggregated_quality_stats AS (
DATE_TRUNC('day', created_at) AS created_date,
MAX(users) AS max_users,
MAX(groups) AS max_groups,
MAX(groups365) AS max_groups365,
MAX(computers) AS max_computers,
MAX(ous) AS max_ous,
MAX(containers) AS max_containers,
Expand All @@ -101,6 +102,7 @@ SELECT
created_date AS created_at,
SUM(max_users) AS users,
SUM(max_groups) AS groups,
SUM(max_groups365) AS groups365,
SUM(max_computers) AS computers,
SUM(max_ous) AS ous,
SUM(max_containers) AS containers,
Expand Down
25 changes: 25 additions & 0 deletions cmd/api/src/database/migration/migrations/v7.4.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Copyright 2025 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

-- Migration to add `Microsoft 365 groups` column to relevant tables

-- Add `groups365` column to `azure_data_quality_aggregations` table
ALTER TABLE IF EXISTS azure_data_quality_aggregations
ADD COLUMN groups365 bigint;

-- Add `groups365` column to `azure_data_quality_stats` table
ALTER TABLE IF EXISTS azure_data_quality_stats
ADD COLUMN groups365 bigint;
2 changes: 1 addition & 1 deletion cmd/api/src/migrations/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func Version_508_Migration(ctx context.Context, db graph.Database) error {
return query.And(
query.Kind(query.Start(), azure.Entity),
// Not all of these node types are being changed, but there's no harm in adding them to the migration
query.KindIn(query.End(), azure.ManagementGroup, azure.ResourceGroup, azure.Subscription, azure.KeyVault, azure.AutomationAccount, azure.ContainerRegistry, azure.LogicApp, azure.VMScaleSet, azure.WebApp, azure.FunctionApp, azure.ManagedCluster, azure.VM),
query.KindIn(query.End(), azure.ManagementGroup, azure.ResourceGroup, azure.Subscription, azure.KeyVault, azure.AutomationAccount, azure.ContainerRegistry, azure.LogicApp, azure.VMScaleSet, azure.WebApp, azure.FunctionApp, azure.ManagedCluster, azure.VM, azure.Group365),
query.Kind(query.Relationship(), azure.Owns),
)
}).Fetch(func(cursor graph.Cursor[*graph.Relationship]) error {
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/model/azurequality.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type AzureStatKinds struct {
Relationships int `json:"relationships"`
Users int `json:"users"`
Groups int `json:"groups"`
Groups365 int `json:"groups365"`
Apps int `json:"apps"`
ServicePrincipals int `json:"service_principals"`
Devices int `json:"devices"`
Expand Down
11 changes: 11 additions & 0 deletions cmd/api/src/test/integration/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,17 @@ func (s *GraphTestContext) NewAzureGroup(name, objectID, tenantID string) *graph
}), azure.Entity, azure.Group)
}

func (s *GraphTestContext) NewAzureGroup365(name, objectID, tenantID string) *graph.Node {

return s.NewNode(graph.AsProperties(graph.PropertyMap{

common.Name: name,
common.ObjectID: objectID,
azure.TenantID: tenantID,
azure.IsAssignableToRole: true,
}), azure.Entity, azure.Group365)
}

func (s *GraphTestContext) NewAzureVM(name, objectID, tenantID string) *graph.Node {
return s.NewNode(graph.AsProperties(graph.PropertyMap{
common.Name: name,
Expand Down
Loading