diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go
index bda73f3ce..d6f856be2 100644
--- a/cmd/api/src/analysis/ad/adcs_integration_test.go
+++ b/cmd/api/src/analysis/ad/adcs_integration_test.go
@@ -639,6 +639,33 @@ func TestADCSESC9a(t *testing.T) {
}
return nil
})
+
+ db.ReadTransaction(context.Background(), func(tx graph.Transaction) error {
+ if results, err := ops.FetchRelationships(tx.Relationships().Filterf(func() graph.Criteria {
+ return query.Kind(query.Relationship(), ad.ADCSESC9a)
+ })); err != nil {
+ t.Fatalf("error fetching esc9a edges in integration test; %v", err)
+ } else {
+ assert.Equal(t, 1, len(results))
+ edge := results[0]
+
+ if edgeComp, err := ad2.GetEdgeCompositionPath(context.Background(), db, edge); err != nil {
+ t.Fatalf("error getting edge composition for esc9: %v", err)
+ } else {
+ nodes := edgeComp.AllNodes().Slice()
+ assert.Contains(t, nodes, harness.ESC9AHarness.Attacker)
+ assert.Contains(t, nodes, harness.ESC9AHarness.Victim)
+ assert.Contains(t, nodes, harness.ESC9AHarness.Domain)
+ assert.Contains(t, nodes, harness.ESC9AHarness.NTAuthStore)
+ assert.Contains(t, nodes, harness.ESC9AHarness.RootCA)
+ assert.Contains(t, nodes, harness.ESC9AHarness.DC)
+ assert.Contains(t, nodes, harness.ESC9AHarness.EnterpriseCA)
+ assert.Contains(t, nodes, harness.ESC9AHarness.CertTemplate)
+ }
+ }
+
+ return nil
+ })
})
}
diff --git a/cmd/api/src/analysis/azure/queries.go b/cmd/api/src/analysis/azure/queries.go
index 75bf58402..6c27548dd 100644
--- a/cmd/api/src/analysis/azure/queries.go
+++ b/cmd/api/src/analysis/azure/queries.go
@@ -1,17 +1,17 @@
// Copyright 2023 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
package azure
@@ -21,7 +21,6 @@ import (
"fmt"
"sync"
- "github.com/specterops/bloodhound/src/model"
"github.com/gofrs/uuid"
"github.com/specterops/bloodhound/analysis"
"github.com/specterops/bloodhound/dawgs/graph"
@@ -30,6 +29,7 @@ import (
"github.com/specterops/bloodhound/graphschema/azure"
"github.com/specterops/bloodhound/graphschema/common"
"github.com/specterops/bloodhound/log"
+ "github.com/specterops/bloodhound/src/model"
)
func GraphStats(ctx context.Context, db graph.Database) (model.AzureDataQualityStats, model.AzureDataQualityAggregation, error) {
@@ -48,7 +48,7 @@ func GraphStats(ctx context.Context, db graph.Database) (model.AzureDataQualityS
runID = newUUID.String()
}
- return stats, aggregation, db.ReadTransaction(ctx, func(tx graph.Transaction) error {
+ err := db.ReadTransaction(ctx, func(tx graph.Transaction) error {
if tenants, err := ops.FetchNodes(tx.Nodes().Filterf(func() graph.Criteria {
return query.Kind(query.Node(), azure.Tenant)
})); err != nil {
@@ -58,6 +58,8 @@ func GraphStats(ctx context.Context, db graph.Database) (model.AzureDataQualityS
if tenantObjectID, err := tenant.Properties.Get(common.ObjectID.String()).String(); err != nil {
log.Errorf("Tenant node %d does not have a valid %s property: %v", tenant.ID, common.ObjectID, err)
} else {
+ aggregation.Tenants++
+
var (
stat = model.AzureDataQualityStat{
RunID: runID,
@@ -163,4 +165,6 @@ func GraphStats(ctx context.Context, db graph.Database) (model.AzureDataQualityS
return nil
})
+
+ return stats, aggregation, err
}
diff --git a/cmd/api/src/analysis/azure/queries_test.go b/cmd/api/src/analysis/azure/queries_test.go
new file mode 100644
index 000000000..4afde925a
--- /dev/null
+++ b/cmd/api/src/analysis/azure/queries_test.go
@@ -0,0 +1,52 @@
+// Copyright 2023 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
+
+//go:build integration
+// +build integration
+
+package azure_test
+
+import (
+ "context"
+ "github.com/specterops/bloodhound/dawgs/graph"
+ schema "github.com/specterops/bloodhound/graphschema"
+ azure2 "github.com/specterops/bloodhound/src/analysis/azure"
+ "github.com/specterops/bloodhound/src/test/integration"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestAnalysisAzure_GraphStats(t *testing.T) {
+ testCtx := integration.NewGraphTestContext(t, schema.DefaultGraphSchema())
+ testCtx.DatabaseTest(func(harness integration.HarnessDetails, db graph.Database) {
+
+ _, agg, err := azure2.GraphStats(context.TODO(), testCtx.Graph.Database)
+ require.Nil(t, err)
+ assert.NotZero(t, agg.Tenants)
+ assert.NotZero(t, agg.Users)
+ assert.NotZero(t, agg.Groups)
+ assert.NotZero(t, agg.Apps)
+ assert.NotZero(t, agg.ServicePrincipals)
+ assert.NotZero(t, agg.Devices)
+ assert.NotZero(t, agg.ManagementGroups)
+ assert.NotZero(t, agg.Subscriptions)
+ assert.NotZero(t, agg.ResourceGroups)
+ assert.NotZero(t, agg.VMs)
+ assert.NotZero(t, agg.KeyVaults)
+ assert.NotZero(t, agg.Relationships)
+ })
+}
diff --git a/cmd/api/src/test/integration/graph.go b/cmd/api/src/test/integration/graph.go
index b1459ba1c..a56fbd8e6 100644
--- a/cmd/api/src/test/integration/graph.go
+++ b/cmd/api/src/test/integration/graph.go
@@ -461,6 +461,7 @@ func (s *GraphTestContext) setupAzure() {
s.Harness.AZMGRoleManagementReadWriteDirectoryHarness.Setup(s)
s.Harness.AZMGServicePrincipalEndpointReadWriteAllHarness.Setup(s)
s.Harness.AZInboundControlHarness.Setup(s)
+ s.Harness.AZManagementGroup.Setup(s)
}
func (s *GraphTestContext) setupActiveDirectory() {
diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go
index 9b1194651..a19dc3075 100644
--- a/cmd/api/src/test/integration/harnesses.go
+++ b/cmd/api/src/test/integration/harnesses.go
@@ -753,16 +753,41 @@ type AZGroupMembershipHarness struct {
func (s *AZGroupMembershipHarness) Setup(testCtx *GraphTestContext) {
tenantID := RandomObjectID(testCtx.testCtx)
+ s.Tenant = testCtx.NewAzureTenant(tenantID)
s.UserA = testCtx.NewAzureUser("UserA", "UserA", "", RandomObjectID(testCtx.testCtx), "", tenantID, false)
s.UserB = testCtx.NewAzureUser("UserB", "UserB", "", RandomObjectID(testCtx.testCtx), "", tenantID, false)
s.UserC = testCtx.NewAzureUser("UserC", "UserC", "", RandomObjectID(testCtx.testCtx), "", tenantID, false)
s.Group = testCtx.NewAzureGroup("Group", RandomObjectID(testCtx.testCtx), tenantID)
+ testCtx.NewRelationship(s.Tenant, s.Group, azure.Contains)
+
testCtx.NewRelationship(s.UserA, s.Group, azure.MemberOf)
testCtx.NewRelationship(s.UserB, s.Group, azure.MemberOf)
testCtx.NewRelationship(s.UserC, s.Group, azure.MemberOf)
}
+type AZManagementGroupHarness struct {
+ Tenant *graph.Node
+ UserA *graph.Node
+ UserB *graph.Node
+ UserC *graph.Node
+ Group *graph.Node
+}
+
+func (s *AZManagementGroupHarness) Setup(testCtx *GraphTestContext) {
+ tenantID := RandomObjectID(testCtx.testCtx)
+ s.Tenant = testCtx.NewAzureTenant(tenantID)
+ s.UserA = testCtx.NewAzureUser("Batman", "Batman", "", RandomObjectID(testCtx.testCtx), "", tenantID, false)
+ s.UserB = testCtx.NewAzureUser("Wonder Woman", "Wonder Woman", "", RandomObjectID(testCtx.testCtx), "", tenantID, false)
+ s.UserC = testCtx.NewAzureUser("Flash", "Flash", "", RandomObjectID(testCtx.testCtx), "", tenantID, false)
+ s.Group = testCtx.NewAzureManagementGroup("Justice League", RandomObjectID(testCtx.testCtx), tenantID)
+ testCtx.NewRelationship(s.Tenant, s.Group, azure.Contains)
+
+ testCtx.NewRelationship(s.UserA, s.Group, azure.ManagementGroup)
+ testCtx.NewRelationship(s.UserB, s.Group, azure.ManagementGroup)
+ testCtx.NewRelationship(s.UserC, s.Group, azure.ManagementGroup)
+}
+
type AZEntityPanelHarness struct {
Application *graph.Node
Device *graph.Node
@@ -1030,6 +1055,7 @@ func (s *AZMGServicePrincipalEndpointReadWriteAllHarness) Setup(testCtx *GraphTe
}
type AZInboundControlHarness struct {
+ AZTenant *graph.Node
ControlledAZUser *graph.Node
AZAppA *graph.Node
AZGroupA *graph.Node
@@ -1042,6 +1068,7 @@ type AZInboundControlHarness struct {
func (s *AZInboundControlHarness) Setup(testCtx *GraphTestContext) {
tenantID := RandomObjectID(testCtx.testCtx)
+ s.AZTenant = testCtx.NewAzureTenant(tenantID)
s.ControlledAZUser = testCtx.NewAzureUser("Controlled AZUser", "Controlled AZUser", "", RandomObjectID(testCtx.testCtx), HarnessUserLicenses, tenantID, HarnessUserMFAEnabled)
s.AZAppA = testCtx.NewAzureApplication("AZAppA", RandomObjectID(testCtx.testCtx), tenantID)
s.AZGroupA = testCtx.NewAzureGroup("AZGroupA", RandomObjectID(testCtx.testCtx), tenantID)
@@ -1051,6 +1078,8 @@ func (s *AZInboundControlHarness) Setup(testCtx *GraphTestContext) {
s.AZServicePrincipalA = testCtx.NewAzureServicePrincipal("AZServicePrincipalA", RandomObjectID(testCtx.testCtx), tenantID)
s.AZServicePrincipalB = testCtx.NewAzureServicePrincipal("AZServicePrincipalB", RandomObjectID(testCtx.testCtx), tenantID)
+ testCtx.NewRelationship(s.AZTenant, s.AZGroupA, azure.Contains)
+
testCtx.NewRelationship(s.AZUserA, s.AZGroupA, azure.MemberOf)
testCtx.NewRelationship(s.AZServicePrincipalB, s.AZGroupB, azure.MemberOf)
@@ -2198,6 +2227,7 @@ type HarnessDetails struct {
Completeness CompletenessHarness
AZBaseHarness AZBaseHarness
AZGroupMembership AZGroupMembershipHarness
+ AZManagementGroup AZManagementGroupHarness
AZEntityPanelHarness AZEntityPanelHarness
AZMGApplicationReadWriteAllHarness AZMGApplicationReadWriteAllHarness
AZMGAppRoleManagementReadWriteAllHarness AZMGAppRoleManagementReadWriteAllHarness
diff --git a/cmd/api/src/test/integration/harnesses/azinboundcontrolharness.json b/cmd/api/src/test/integration/harnesses/AZInboundControlHarness.json
similarity index 77%
rename from cmd/api/src/test/integration/harnesses/azinboundcontrolharness.json
rename to cmd/api/src/test/integration/harnesses/AZInboundControlHarness.json
index b57cb1b59..8cfdb5e1f 100644
--- a/cmd/api/src/test/integration/harnesses/azinboundcontrolharness.json
+++ b/cmd/api/src/test/integration/harnesses/AZInboundControlHarness.json
@@ -54,10 +54,10 @@
},
"nodes": [
{
- "id": "n7",
+ "id": "n0",
"position": {
"x": 1059.8976569191977,
- "y": 1346.027889910619
+ "y": 501.6907518623764
},
"caption": "Controlled AZUser",
"labels": [],
@@ -67,10 +67,10 @@
}
},
{
- "id": "n8",
+ "id": "n1",
"position": {
"x": 604.0284270522187,
- "y": 894.3371380482427
+ "y": 50
},
"caption": "AZGroup A",
"labels": [],
@@ -80,10 +80,10 @@
}
},
{
- "id": "n9",
+ "id": "n2",
"position": {
"x": 604.0284270522187,
- "y": 1838.502464519107
+ "y": 994.1653264708643
},
"caption": "AZGroup B",
"labels": [],
@@ -93,10 +93,10 @@
}
},
{
- "id": "n10",
+ "id": "n3",
"position": {
"x": 604.0284270522187,
- "y": 1475.6758276195499
+ "y": 631.3386895713072
},
"caption": "AZServicePrincipal A",
"labels": [],
@@ -106,10 +106,10 @@
}
},
{
- "id": "n11",
+ "id": "n4",
"position": {
"x": 604.0284270522187,
- "y": 1229.2947464229803
+ "y": 384.9576083747377
},
"caption": "AZUser B",
"labels": [],
@@ -119,10 +119,10 @@
}
},
{
- "id": "n12",
+ "id": "n5",
"position": {
"x": 75,
- "y": 894.3371380482427
+ "y": 50
},
"caption": "AZUser A",
"labels": [],
@@ -132,10 +132,10 @@
}
},
{
- "id": "n13",
+ "id": "n6",
"position": {
"x": 75,
- "y": 1838.502464519107
+ "y": 994.1653264708643
},
"caption": "AZServicePrincipal B",
"labels": [],
@@ -145,14 +145,27 @@
}
},
{
- "id": "n14",
+ "id": "n7",
"position": {
"x": 75,
- "y": 1475.6758276195499
+ "y": 631.3386895713072
},
"caption": "AZApp A",
+ "labels": [],
+ "properties": {},
"style": {
"border-color": "#d33115"
+ }
+ },
+ {
+ "id": "n8",
+ "position": {
+ "x": 379.0709828688383,
+ "y": 274.95744418338046
+ },
+ "caption": "AZTenant",
+ "style": {
+ "border-color": "#aea1ff"
},
"labels": [],
"properties": {}
@@ -160,74 +173,84 @@
],
"relationships": [
{
- "id": "n8",
+ "id": "n0",
+ "fromId": "n3",
+ "toId": "n0",
"type": "AZResetPassword",
+ "properties": {},
"style": {
"arrow-color": "#68bc00"
- },
- "properties": {},
- "fromId": "n10",
- "toId": "n7"
+ }
},
{
- "id": "n9",
+ "id": "n1",
+ "fromId": "n4",
+ "toId": "n0",
"type": "AZResetPassword",
+ "properties": {},
"style": {
"arrow-color": "#68bc00"
- },
- "properties": {},
- "fromId": "n11",
- "toId": "n7"
+ }
},
{
- "id": "n10",
+ "id": "n2",
+ "fromId": "n5",
+ "toId": "n1",
"type": "AZMemberOf",
+ "properties": {},
"style": {
"arrow-color": "#68bc00"
- },
- "properties": {},
- "fromId": "n12",
- "toId": "n8"
+ }
},
{
- "id": "n11",
+ "id": "n3",
+ "fromId": "n6",
+ "toId": "n2",
"type": "AZMemberOf",
+ "properties": {},
"style": {
"arrow-color": "#68bc00"
- },
- "properties": {},
- "fromId": "n13",
- "toId": "n9"
+ }
},
{
- "id": "n13",
+ "id": "n4",
+ "fromId": "n1",
+ "toId": "n0",
"type": "AZResetPassword",
+ "properties": {},
"style": {
"arrow-color": "#68bc00"
- },
- "properties": {},
- "fromId": "n8",
- "toId": "n7"
+ }
},
{
- "id": "n16",
+ "id": "n5",
+ "fromId": "n2",
+ "toId": "n0",
"type": "AZResetPassword",
+ "properties": {},
"style": {
"arrow-color": "#68bc00"
- },
- "properties": {},
- "fromId": "n9",
- "toId": "n7"
+ }
},
{
- "id": "n17",
+ "id": "n6",
+ "fromId": "n7",
+ "toId": "n3",
"type": "AZRunsAs",
+ "properties": {},
"style": {
"arrow-color": "#d33115"
+ }
+ },
+ {
+ "id": "n7",
+ "type": "AZContains",
+ "style": {
+ "arrow-color": "#68bc00"
},
"properties": {},
- "fromId": "n14",
- "toId": "n10"
+ "fromId": "n8",
+ "toId": "n1"
}
]
}
\ No newline at end of file
diff --git a/cmd/api/src/test/integration/harnesses/AZInboundControlHarness.svg b/cmd/api/src/test/integration/harnesses/AZInboundControlHarness.svg
new file mode 100644
index 000000000..3c0d8c6ce
--- /dev/null
+++ b/cmd/api/src/test/integration/harnesses/AZInboundControlHarness.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/cmd/api/src/test/integration/harnesses/AZManagementGroup.json b/cmd/api/src/test/integration/harnesses/AZManagementGroup.json
new file mode 100644
index 000000000..1a52ec979
--- /dev/null
+++ b/cmd/api/src/test/integration/harnesses/AZManagementGroup.json
@@ -0,0 +1,156 @@
+{
+ "nodes": [
+ {
+ "id": "n0",
+ "position": {
+ "x": -55,
+ "y": 85.2904443080064
+ },
+ "caption": "UserA",
+ "labels": [
+ "AZUser"
+ ],
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n1",
+ "position": {
+ "x": -55,
+ "y": 253.81479412258489
+ },
+ "caption": "UserB",
+ "labels": [
+ "AZUser"
+ ],
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n2",
+ "position": {
+ "x": -55,
+ "y": 422.3391439371636
+ },
+ "caption": "UserC",
+ "labels": [
+ "AZUser"
+ ],
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n3",
+ "position": {
+ "x": 449.99999999999994,
+ "y": 40.19237886466851
+ },
+ "caption": "Tenant",
+ "labels": [
+ "AZTenant"
+ ],
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n4",
+ "position": {
+ "x": 300,
+ "y": 253.81479412258489
+ },
+ "caption": "Group",
+ "labels": [
+ "AZGroup"
+ ],
+ "properties": {},
+ "style": {}
+ }
+ ],
+ "relationships": [
+ {
+ "id": "n0",
+ "fromId": "n2",
+ "toId": "n4",
+ "type": "AZManagementGroup",
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n1",
+ "fromId": "n1",
+ "toId": "n4",
+ "type": "AZManagementGroup",
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n2",
+ "fromId": "n0",
+ "toId": "n4",
+ "type": "AZManagementGroup",
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n3",
+ "fromId": "n3",
+ "toId": "n4",
+ "type": "AZContains",
+ "properties": {},
+ "style": {}
+ }
+ ],
+ "style": {
+ "font-family": "sans-serif",
+ "background-color": "#ffffff",
+ "background-image": "",
+ "background-size": "100%",
+ "node-color": "#ffffff",
+ "border-width": 4,
+ "border-color": "#68bc00",
+ "radius": 50,
+ "node-padding": 5,
+ "node-margin": 2,
+ "outside-position": "auto",
+ "node-icon-image": "",
+ "node-background-image": "",
+ "icon-position": "inside",
+ "icon-size": 64,
+ "caption-position": "inside",
+ "caption-max-width": 200,
+ "caption-color": "#000000",
+ "caption-font-size": 50,
+ "caption-font-weight": "normal",
+ "label-position": "inside",
+ "label-display": "pill",
+ "label-color": "#000000",
+ "label-background-color": "#ffffff",
+ "label-border-color": "#000000",
+ "label-border-width": 4,
+ "label-font-size": 40,
+ "label-padding": 5,
+ "label-margin": 4,
+ "directionality": "directed",
+ "detail-position": "inline",
+ "detail-orientation": "parallel",
+ "arrow-width": 5,
+ "arrow-color": "#aea1ff",
+ "margin-start": 5,
+ "margin-end": 5,
+ "margin-peer": 20,
+ "attachment-start": "normal",
+ "attachment-end": "normal",
+ "relationship-icon-image": "",
+ "type-color": "#000000",
+ "type-background-color": "#ffffff",
+ "type-border-color": "#000000",
+ "type-border-width": 0,
+ "type-font-size": 16,
+ "type-padding": 5,
+ "property-position": "outside",
+ "property-alignment": "colon",
+ "property-color": "#000000",
+ "property-font-size": 16,
+ "property-font-weight": "normal"
+ }
+}
diff --git a/cmd/api/src/test/integration/harnesses/AZManagementGroup.svg b/cmd/api/src/test/integration/harnesses/AZManagementGroup.svg
new file mode 100644
index 000000000..1a7ac2bbb
--- /dev/null
+++ b/cmd/api/src/test/integration/harnesses/AZManagementGroup.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/cmd/api/src/test/integration/harnesses/azmembership.json b/cmd/api/src/test/integration/harnesses/azgroupmembership.json
similarity index 77%
rename from cmd/api/src/test/integration/harnesses/azmembership.json
rename to cmd/api/src/test/integration/harnesses/azgroupmembership.json
index e01123799..c281b8294 100644
--- a/cmd/api/src/test/integration/harnesses/azmembership.json
+++ b/cmd/api/src/test/integration/harnesses/azgroupmembership.json
@@ -54,36 +54,36 @@
},
"nodes": [
{
- "id": "n0",
+ "id": "n1",
"position": {
- "x": 0,
- "y": 0
+ "x": 505.40943703856345,
+ "y": 89.79253144038367
},
"caption": "Group",
- "style": {},
"labels": [
"AZGroup"
],
- "properties": {}
+ "properties": {},
+ "style": {}
},
{
- "id": "n1",
+ "id": "n2",
"position": {
- "x": -276.40943703856345,
- "y": -159.58506288076748
+ "x": 129,
+ "y": -4
},
"caption": "UserA",
- "style": {},
"labels": [
"AZUser"
],
- "properties": {}
+ "properties": {},
+ "style": {}
},
{
- "id": "n2",
+ "id": "n3",
"position": {
- "x": -276.40943703856345,
- "y": 0
+ "x": 129,
+ "y": 155.58506288076748
},
"caption": "UserB",
"labels": [
@@ -93,10 +93,10 @@
"style": {}
},
{
- "id": "n3",
+ "id": "n4",
"position": {
- "x": -276.40943703856345,
- "y": 159.58506288076748
+ "x": 129,
+ "y": 365.17012576153496
},
"caption": "UserC",
"labels": [
@@ -104,32 +104,51 @@
],
"properties": {},
"style": {}
+ },
+ {
+ "id": "n5",
+ "position": {
+ "x": 129,
+ "y": -185.5850628807675
+ },
+ "caption": "Tenant",
+ "style": {},
+ "labels": [],
+ "properties": {}
}
],
"relationships": [
{
"id": "n0",
+ "fromId": "n2",
+ "toId": "n1",
"type": "AZMemberOf",
- "style": {},
"properties": {},
- "fromId": "n1",
- "toId": "n0"
+ "style": {}
},
{
"id": "n1",
+ "fromId": "n3",
+ "toId": "n1",
"type": "AZMemberOf",
- "style": {},
"properties": {},
- "fromId": "n2",
- "toId": "n0"
+ "style": {}
},
{
"id": "n2",
+ "fromId": "n4",
+ "toId": "n1",
"type": "AZMemberOf",
+ "properties": {},
+ "style": {}
+ },
+ {
+ "id": "n3",
+ "type": "AZContains",
"style": {},
"properties": {},
- "fromId": "n3",
- "toId": "n0"
+ "fromId": "n5",
+ "toId": "n1"
}
]
}
\ No newline at end of file
diff --git a/cmd/api/src/test/integration/harnesses/azgroupmembership.svg b/cmd/api/src/test/integration/harnesses/azgroupmembership.svg
new file mode 100644
index 000000000..d0f558b8a
--- /dev/null
+++ b/cmd/api/src/test/integration/harnesses/azgroupmembership.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/cmd/api/src/test/integration/harnesses/azinboundcontrolharness.svg b/cmd/api/src/test/integration/harnesses/azinboundcontrolharness.svg
deleted file mode 100644
index ecadd9568..000000000
--- a/cmd/api/src/test/integration/harnesses/azinboundcontrolharness.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
diff --git a/cmd/api/src/test/integration/harnesses/azmembership.svg b/cmd/api/src/test/integration/harnesses/azmembership.svg
deleted file mode 100644
index c34f86023..000000000
--- a/cmd/api/src/test/integration/harnesses/azmembership.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx
index bc0e4217b..2f46bf710 100644
--- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx
+++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoContent.tsx
@@ -73,7 +73,7 @@ const EdgeInfoContent: FC<{ selectedEdge: NonNullable }> = ({ sele
const sendOnChange =
(selectedEdge.name === 'GoldenCert' ||
selectedEdge.name === 'ADCSESC1' ||
- selectedEdge.name === 'ADCSESC3') &&
+ selectedEdge.name === 'ADCSESC3' || selectedEdge.name === 'ADCSESC9a') &&
section[0] === 'composition';
return (
diff --git a/packages/go/analysis/ad/ad.go b/packages/go/analysis/ad/ad.go
index 1c0d029a5..cdc9dadea 100644
--- a/packages/go/analysis/ad/ad.go
+++ b/packages/go/analysis/ad/ad.go
@@ -549,6 +549,12 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph.
} else {
pathSet = results
}
+ } else if edge.Kind == ad.ADCSESC9a {
+ if results, err := GetADCSESC9aEdgeComposition(ctx, db, edge); err != nil {
+ return err
+ } else {
+ pathSet = results
+ }
}
return nil
})
@@ -556,7 +562,7 @@ func GetEdgeCompositionPath(ctx context.Context, db graph.Database, edge *graph.
func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -591,7 +597,7 @@ func ADCSESC3Path1Pattern(domainId graph.ID, enterpriseCAs cardinality.Duplex[ui
func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates cardinality.Duplex[uint32]) traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -618,7 +624,7 @@ func ADCSESC3Path2Pattern(domainId graph.ID, enterpriseCAs, candidateTemplates c
func ADCSESC3Path3Pattern() traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -807,7 +813,7 @@ func getDelegatedEnrollmentAgentPath(ctx context.Context, startNode, certTemplat
func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -846,7 +852,7 @@ func ADCSESC1Path1Pattern(domainID graph.ID) traversal.PatternContinuation {
func ADCSESC1Path2Pattern(domainID graph.ID, enterpriseCAs cardinality.Duplex[uint32]) traversal.PatternContinuation {
return traversal.NewPattern().
- Outbound(query.And(
+ OutboundWithDepth(0, 0, query.And(
query.Kind(query.Relationship(), ad.MemberOf),
query.Kind(query.End(), ad.Group),
)).
@@ -931,8 +937,6 @@ func GetADCSESC1EdgeComposition(ctx context.Context, db graph.Database, edge *gr
// Render paths from the segments
return paths, path1EnterpriseCAs.Each(func(value uint32) (bool, error) {
for _, segment := range candidateSegments[graph.ID(value)] {
- log.Infof("Found ESC1 Path: %s", graph.FormatPathSegment(segment))
-
paths.AddPath(segment.Path())
}
@@ -975,3 +979,248 @@ func getGoldenCertEdgeComposition(tx graph.Transaction, edge *graph.Relationship
return finalPaths, nil
}
}
+
+func adcsESC9aPath1Pattern(domainID graph.ID) traversal.PatternContinuation {
+ return traversal.NewPattern().
+ OutboundWithDepth(
+ 1, 1,
+ query.And(
+ query.KindIn(query.Relationship(), ad.GenericWrite, ad.GenericAll, ad.Owns, ad.WriteOwner, ad.WriteDACL),
+ query.KindIn(query.End(), ad.Computer, ad.User),
+ ),
+ ).
+ OutboundWithDepth(
+ 0, 0,
+ query.And(
+ query.Kind(query.Relationship(), ad.MemberOf),
+ query.Kind(query.End(), ad.Group),
+ ),
+ ).
+ Outbound(
+ query.And(
+ query.KindIn(query.Relationship(), ad.GenericAll, ad.Enroll, ad.AllExtendedRights),
+ query.Kind(query.End(), ad.CertTemplate),
+ query.Equals(query.EndProperty(ad.RequiresManagerApproval.String()), false),
+ query.Equals(query.EndProperty(ad.AuthenticationEnabled.String()), true),
+ query.Equals(query.EndProperty(ad.NoSecurityExtension.String()), true),
+ query.Equals(query.EndProperty(ad.EnrolleeSuppliesSubject.String()), false),
+ query.Or(
+ query.Equals(query.EndProperty(ad.SubjectAltRequireUPN.String()), true),
+ query.Equals(query.EndProperty(ad.SubjectAltRequireSPN.String()), true),
+ ),
+ query.Or(
+ query.Equals(query.EndProperty(ad.SchemaVersion.String()), 1),
+ query.And(
+ query.GreaterThan(query.EndProperty(ad.SchemaVersion.String()), 1),
+ query.Equals(query.EndProperty(ad.AuthorizedSignatures.String()), 0),
+ ),
+ ),
+ ),
+ ).
+ Outbound(query.And(
+ query.KindIn(query.Relationship(), ad.PublishedTo, ad.IssuedSignedBy),
+ query.Kind(query.End(), ad.EnterpriseCA),
+ )).
+ Outbound(query.And(
+ query.KindIn(query.Relationship(), ad.IssuedSignedBy, ad.EnterpriseCAFor),
+ query.Kind(query.End(), ad.RootCA),
+ )).
+ Outbound(query.And(
+ query.KindIn(query.Relationship(), ad.RootCAFor),
+ query.Equals(query.EndID(), domainID),
+ ))
+}
+
+func adcsESC9APath2Pattern(caNodes []graph.ID, domainId graph.ID) traversal.PatternContinuation {
+ return traversal.NewPattern().
+ OutboundWithDepth(0, 0, query.And(
+ query.Kind(query.Relationship(), ad.MemberOf),
+ query.Kind(query.End(), ad.Group),
+ )).
+ Outbound(query.And(
+ query.Kind(query.Relationship(), ad.Enroll),
+ query.InIDs(query.End(), caNodes...),
+ )).
+ Outbound(query.And(
+ query.KindIn(query.Relationship(), ad.TrustedForNTAuth),
+ query.Kind(query.End(), ad.NTAuthStore),
+ )).
+ Outbound(query.And(
+ query.KindIn(query.Relationship(), ad.NTAuthStoreFor),
+ query.Equals(query.EndID(), domainId),
+ ))
+}
+
+func adcsESC9APath3Pattern(caIDs []graph.ID) traversal.PatternContinuation {
+ return traversal.NewPattern().
+ Inbound(
+ query.KindIn(query.Relationship(), ad.DCFor, ad.TrustedBy),
+ ).
+ Inbound(query.And(
+ query.Kind(query.Relationship(), ad.CanAbuseWeakCertBinding),
+ query.InIDs(query.StartID(), caIDs...),
+ ))
+}
+
+func GetADCSESC9aEdgeComposition(ctx context.Context, db graph.Database, edge *graph.Relationship) (graph.PathSet, error) {
+ /*
+ MATCH (n {objectid:'S-1-5-21-3933516454-2894985453-2515407000-500'})-[:ADCSESC9a]->(d:Domain {objectid:'S-1-5-21-3933516454-2894985453-2515407000'})
+ OPTIONAL MATCH p1 = (n)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(m)-[:MemberOf*0..]->()-[:GenericAll|Enroll|AllExtendedRights]->(ct)-[:PublishedTo]->(ca)-[:IssuedSignedBy|EnterpriseCAFor|RootCAFor*1..]->(d)
+ WHERE ct.requiresmanagerapproval = false
+ AND ct.authenticationenabled = true
+ AND ct.nosecurityextension = true
+ AND ct.enrolleesuppliessubject = false
+ AND (ct.subjectaltrequireupn = true OR ct.subjectaltrequirespn = true)
+ AND (
+ (ct.schemaversion > 1 AND ct.authorizedsignatures = 0)
+ OR ct.schemaversion = 1
+ )
+ AND (
+ m:Computer
+ OR (m:User AND ct.subjectaltrequiredns = false AND ct.subjectaltrequiredomaindns = false)
+ )
+ OPTIONAL MATCH p2 = (m)-[:MemberOf*0..]->()-[:Enroll]->(ca)-[:TrustedForNTAuth]->(nt)-[:NTAuthStoreFor]->(d)
+ OPTIONAL MATCH p3 = (ca)-[:CanAbuseWeakCertBinding|DCFor|TrustedBy*1..]->(d)
+ RETURN p1,p2,p3
+ */
+
+ var (
+ startNode *graph.Node
+ endNode *graph.Node
+
+ traversalInst = traversal.New(db, analysis.MaximumDatabaseParallelWorkers)
+ paths = graph.PathSet{}
+ path1CandidateSegments = map[graph.ID][]*graph.PathSegment{}
+ victimCANodes = map[graph.ID][]graph.ID{}
+ path2CandidateSegments = map[graph.ID][]*graph.PathSegment{}
+ path3CandidateSegments = map[graph.ID][]*graph.PathSegment{}
+ p2canodes = make([]graph.ID, 0)
+ nodeMap = map[graph.ID]*graph.Node{}
+ lock = &sync.Mutex{}
+ )
+
+ if err := db.ReadTransaction(ctx, func(tx graph.Transaction) error {
+ if node, err := ops.FetchNode(tx, edge.StartID); err != nil {
+ return err
+ } else if eNode, err := ops.FetchNode(tx, edge.EndID); err != nil {
+ return err
+ } else {
+ startNode = node
+ endNode = eNode
+ return nil
+ }
+ }); err != nil {
+ return nil, err
+ }
+
+ //Fully manifest p1
+ if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
+ Root: startNode,
+ Driver: adcsESC9aPath1Pattern(edge.EndID).Do(func(terminal *graph.PathSegment) error {
+ victimNode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
+ return nextSegment.Depth() == 1
+ })
+
+ if victimNode.Kinds.ContainsOneOf(ad.User) {
+ certTemplate := terminal.Search(func(nextSegment *graph.PathSegment) bool {
+ return nextSegment.Node.Kinds.ContainsOneOf(ad.CertTemplate)
+ })
+
+ if !certTemplateValidForUserVictim(certTemplate) {
+ return nil
+ }
+ }
+
+ caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
+ return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA)
+ })
+
+ lock.Lock()
+ path1CandidateSegments[victimNode.ID] = append(path1CandidateSegments[victimNode.ID], terminal)
+ nodeMap[victimNode.ID] = victimNode
+ victimCANodes[victimNode.ID] = append(victimCANodes[victimNode.ID], caNode.ID)
+ lock.Unlock()
+
+ return nil
+ }),
+ }); err != nil {
+ return nil, err
+ }
+
+ for victim, p1CANodes := range victimCANodes {
+ if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
+ Root: nodeMap[victim],
+ Driver: adcsESC9APath2Pattern(p1CANodes, edge.EndID).Do(func(terminal *graph.PathSegment) error {
+ caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
+ return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA)
+ })
+
+ lock.Lock()
+ path2CandidateSegments[caNode.ID] = append(path2CandidateSegments[caNode.ID], terminal)
+ p2canodes = append(p2canodes, caNode.ID)
+ lock.Unlock()
+
+ return nil
+ }),
+ }); err != nil {
+ return nil, err
+ }
+ }
+
+ if len(p2canodes) > 0 {
+ if err := traversalInst.BreadthFirst(ctx, traversal.Plan{
+ Root: endNode,
+ Driver: adcsESC9APath3Pattern(p2canodes).Do(func(terminal *graph.PathSegment) error {
+ caNode := terminal.Search(func(nextSegment *graph.PathSegment) bool {
+ return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA)
+ })
+
+ lock.Lock()
+ path3CandidateSegments[caNode.ID] = append(path3CandidateSegments[caNode.ID], terminal)
+ lock.Unlock()
+ return nil
+ }),
+ }); err != nil {
+ return nil, err
+ }
+ }
+
+ for _, p1paths := range path1CandidateSegments {
+ for _, p1path := range p1paths {
+ caNode := p1path.Search(func(nextSegment *graph.PathSegment) bool {
+ return nextSegment.Node.Kinds.ContainsOneOf(ad.EnterpriseCA)
+ })
+
+ if p2segments, ok := path2CandidateSegments[caNode.ID]; !ok {
+ continue
+ } else if p3segments, ok := path3CandidateSegments[caNode.ID]; !ok {
+ continue
+ } else {
+ paths.AddPath(p1path.Path())
+ for _, p2 := range p2segments {
+ paths.AddPath(p2.Path())
+ }
+
+ for _, p3 := range p3segments {
+ paths.AddPath(p3.Path())
+ }
+ }
+ }
+ }
+
+ return paths, nil
+}
+
+func certTemplateValidForUserVictim(certTemplate *graph.Node) bool {
+ if subjectAltRequireDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil {
+ return false
+ } else if subjectAltRequireDNS {
+ return false
+ } else if subjectAltRequireDomainDNS, err := certTemplate.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil {
+ return false
+ } else if subjectAltRequireDomainDNS {
+ return false
+ } else {
+ return true
+ }
+}
diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go
index 3ec0b72b4..dcfe83f10 100644
--- a/packages/go/dawgs/traversal/traversal.go
+++ b/packages/go/dawgs/traversal/traversal.go
@@ -49,7 +49,9 @@ type PatternMatchDelegate = func(terminal *graph.PathSegment) error
// The return value of the Do(...) function may be passed directly to a Traversal via a Plan as the Plan.Driver field.
type PatternContinuation interface {
Outbound(criteria ...graph.Criteria) PatternContinuation
+ OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation
Inbound(criteria ...graph.Criteria) PatternContinuation
+ InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation
Do(delegate PatternMatchDelegate) Driver
}
@@ -57,6 +59,8 @@ type PatternContinuation interface {
type expansion struct {
criteria []graph.Criteria
direction graph.Direction
+ minDepth int
+ maxDepth int
}
func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria, error) {
@@ -84,6 +88,7 @@ func (s expansion) PrepareCriteria(segment *graph.PathSegment) (graph.Criteria,
type patternTag struct {
patternIdx int
+ depth int
}
func popSegmentPatternTag(segment *graph.PathSegment) *patternTag {
@@ -95,6 +100,7 @@ func popSegmentPatternTag(segment *graph.PathSegment) *patternTag {
} else {
tag = &patternTag{
patternIdx: 0,
+ depth: 0,
}
}
@@ -112,26 +118,62 @@ func (s *pattern) Do(delegate PatternMatchDelegate) Driver {
return s.Driver
}
-// Outbound specifies the next outbound expansion step for this pattern.
-func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation {
+// OutboundWithDepth specifies the next outbound expansion step for this pattern with depth parameters.
+func (s *pattern) OutboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation {
+ if min < 0 {
+ min = 1
+ log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1")
+ }
+
+ if max < 0 {
+ max = 0
+ log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0")
+ }
+
s.expansions = append(s.expansions, expansion{
criteria: criteria,
direction: graph.DirectionOutbound,
+ minDepth: min,
+ maxDepth: max,
})
return s
}
-// Inbound specifies the next inbound expansion step for this pattern.
-func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation {
+// Outbound specifies the next outbound expansion step for this pattern. By default, this expansion will use a minimum
+// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely.
+func (s *pattern) Outbound(criteria ...graph.Criteria) PatternContinuation {
+ return s.OutboundWithDepth(1, 0, criteria...)
+}
+
+// InboundWithDepth specifies the next inbound expansion step for this pattern with depth parameters.
+func (s *pattern) InboundWithDepth(min, max int, criteria ...graph.Criteria) PatternContinuation {
+ if min < 0 {
+ min = 1
+ log.Warnf("Negative mindepth not allowed. Setting min depth for expansion to 1")
+ }
+
+ if max < 0 {
+ max = 0
+ log.Warnf("Negative maxdepth not allowed. Setting max depth for expansion to 0")
+ }
+
s.expansions = append(s.expansions, expansion{
criteria: criteria,
direction: graph.DirectionInbound,
+ minDepth: min,
+ maxDepth: max,
})
return s
}
+// Inbound specifies the next inbound expansion step for this pattern. By default, this expansion will use a minimum
+// depth of 1 to make the expansion required and a maximum depth of 0 to expand indefinitely.
+func (s *pattern) Inbound(criteria ...graph.Criteria) PatternContinuation {
+ return s.InboundWithDepth(1, 0, criteria...)
+}
+
// NewPattern returns a new PatternContinuation for building a new pattern.
func NewPattern() PatternContinuation {
return &pattern{}
@@ -152,9 +194,9 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra
for next := range cursor.Chan() {
nextSegment := segment.Descend(next.Node, next.Relationship)
nextSegment.Tag = &patternTag{
- // Use the tag's patternIdx here since this is the reference that will see the increment when
- // the current expansion is exhausted
+ // Use the tag's patternIdx and depth since this is a continuation of the expansions
patternIdx: tag.patternIdx,
+ depth: tag.depth + 1,
}
nextSegments = append(nextSegments, nextSegment)
@@ -168,34 +210,51 @@ func (s *pattern) Driver(ctx context.Context, tx graph.Transaction, segment *gra
if fetchDirection, err := currentExpansion.direction.Reverse(); err != nil {
return nil, err
} else {
- // Perform the current expansion.
- if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil {
- return nil, err
- } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil {
- return nil, err
- }
-
- // No further expansions means this pattern segment is complete. Increment the pattern index to select the
- // next pattern expansion.
- tag.patternIdx++
-
- // Perform the next expansion if there is one.
- if tag.patternIdx < len(s.expansions) {
- nextExpansion := s.expansions[tag.patternIdx]
-
- // Expand the next segments
- if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil {
+ // If no max depth was set or if a max depth was set expand the current step further
+ if currentExpansion.maxDepth == 0 || tag.depth < currentExpansion.maxDepth {
+ // Perform the current expansion.
+ if criteria, err := currentExpansion.PrepareCriteria(segment); err != nil {
return nil, err
} else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil {
return nil, err
}
- } else if len(nextSegments) == 0 {
- // If there are no expanded segments and there are no remaining expansions, this is a terminal segment.
- // Hand it off to the delegate and handle any returned error.
- if err := s.delegate(segment); err != nil {
- return nil, err
+ }
+
+ // Check first if this current segment was fetched using the current expansion (i.e. non-optional)
+ if tag.depth > 0 && currentExpansion.minDepth == 0 || tag.depth >= currentExpansion.minDepth {
+ // No further expansions means this pattern segment is complete. Increment the pattern index to select the
+ // next pattern expansion. Additionally, set the depth back to zero for the tag since we are leaving the
+ // current expansion.
+ tag.patternIdx++
+ tag.depth = 0
+
+ // Perform the next expansion if there is one.
+ if tag.patternIdx < len(s.expansions) {
+ nextExpansion := s.expansions[tag.patternIdx]
+
+ // Expand the next segments
+ if criteria, err := nextExpansion.PrepareCriteria(segment); err != nil {
+ return nil, err
+ } else if err := tx.Relationships().Filter(criteria).FetchDirection(fetchDirection, fetchFunc); err != nil {
+ return nil, err
+ }
+
+ // If the next expansion is optional, make sure to preserve the current traversal branch
+ if nextExpansion.minDepth == 0 {
+ // Reattach the tag to the segment before adding it to the returned segments for the next expansion
+ segment.Tag = tag
+ nextSegments = append(nextSegments, segment)
+ }
+ } else if len(nextSegments) == 0 {
+ // If there are no expanded segments and there are no remaining expansions, this is a terminal segment.
+ // Hand it off to the delegate and handle any returned error.
+ if err := s.delegate(segment); err != nil {
+ return nil, err
+ }
}
}
+
+ // If the above condition does not match then this current expansion is non-terminal and non-continuable
}
// Return any collected segments
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx
index 25356805b..73d474ef4 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/General.tsx
@@ -27,15 +27,14 @@ const General: FC = ({ sourceName, sourceType, targetName }) => {
domain {targetName}.
- The principal has permission to enroll on a certificate template with the Certificate Request Agent EKU
- (OID 1.3.6.1.4.1.311.20.2.1), allowing them to obtain an enrollment agent certificate. They also have
- permission to enroll for a certificate template that permits enrollment by enrollment agents and can be
- used for authentication. Additionally, they also have enrollment permissions for an enterprise CA with
- the necessary templates published. This enterprise CA is trusted for NT authentication in the forest,
- along with the CA certificate chain up to the root CA certificate. This setup lets the principal enroll
- certificates for any AD forest user or computer, enabling authentication and impersonation of any AD
- forest user or computer without their credentials, unless the target user or computer is protected by
- enrollment agent restrictions on the enterprise CA.
+ The principal has permission to enroll on a certificate allowing them to obtain an enrollment agent
+ certificate. They also have permission to enroll for a certificate template that permits enrollment by
+ enrollment agents and can be used for authentication. Additionally, they also have enrollment
+ permissions for an enterprise CA with the necessary templates published. This enterprise CA is trusted
+ for NT authentication in the forest, along with the CA certificate chain up to the root CA certificate.
+ This setup lets the principal enroll certificates for any AD forest user or computer, enabling
+ authentication and impersonation of any AD forest user or computer without their credentials, unless the
+ target user or computer is protected by enrollment agent restrictions on the enterprise CA.
>
);
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx
index 5b1438442..5a555d739 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/LinuxAbuse.tsx
@@ -32,6 +32,15 @@ const LinuxAbuse: FC = () => {
"certipy req -u 'user@corp.local' -p 'password' -dc-ip 'DC_IP' -target 'ca_host' -ca 'ca_name' -template 'vulnerable template'"
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
@@ -44,6 +53,12 @@ const LinuxAbuse: FC = () => {
"certipy req -u 'user@corp.local' -p 'password' -dc-ip 'DC_IP' -target 'ca_host' -ca 'ca_name' -template 'User' -on-behalf-of 'contoso\\administrator' -pfx 'user.pfx'"
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the target principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template.
+ Choose another target with the given attribute set.
+
Step 3:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx
index fc9c11c59..e6bd09040 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC3/WindowsAbuse.tsx
@@ -30,6 +30,15 @@ const WindowsAbuse: FC = () => {
{'Certify.exe request /ca:CORPDC01.CORP.LOCAL\\CORP-CORPDC01-CA /template:Vuln-EnrollmentAgent'}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
@@ -55,6 +64,12 @@ const WindowsAbuse: FC = () => {
Save the certificate as itadminenrollment.pem and the private key as{' '}
itadminenrollment.key.
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the target principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template.
+ Choose another target with the given attribute set.
+
Step 4:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx
index 874627307..83ce889f6 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/LinuxAbuse.tsx
@@ -30,6 +30,15 @@ const LinuxAbuse: FC = () => {
'certipy req -u john@corp.local -p Passw0rd -ca corp-DC-CA -target ca.corp.local -template ESC6 -upn administrator@corp.local'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2: Request a ticket granting ticket (TGT) from the domain, specifying the certificate
created in Step 1 and the IP of a domain controller:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx
index 12e36054d..296249a25 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6a/WindowsAbuse.tsx
@@ -30,6 +30,15 @@ const WindowsAbuse: FC = () => {
'.\\Certify.exe request /ca:rootdomaindc.forestroot.com\\forestroot-RootDomainDC-CA /template:ESC6 /altname:forestroot\\ForestRootDA'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2: Convert the emitted certificate to PFX format:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx
index 275e0334c..2c91fa3bf 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/LinuxAbuse.tsx
@@ -33,6 +33,15 @@ const LinuxAbuse: FC = () => {
'certipy req -u john@corp.local -p Passw0rd -ca corp-DC-CA -target ca.corp.local -template ESC6 -upn administrator@corp.local'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx
index a9b6f1bef..0d81e85a1 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC6b/WindowsAbuse.tsx
@@ -33,6 +33,15 @@ const WindowsAbuse: FC = () => {
'.\\Certify.exe request /ca:rootdomaindc.forestroot.com\\forestroot-RootDomainDC-CA /template:ESC6 /altname:forestroot\\ForestRootDA'
}
+
+ If the enrollment fails with an error message stating that the Email or DNS name is unavailable and
+ cannot be added to the Subject or Subject Alternate name, then it is because the enrollee principal does
+ not have their 'mail' or 'dNSHostName' attribute set, which is required by the certificate template. The
+ 'mail' attribute can be set on both user and computer objects but the 'dNSHostName' attribute can only
+ be set on computer objects. Computers have validated write permission to their own 'dNSHostName'
+ attribute by default, but neither users nor computers can write to their own 'mail' attribute by
+ default.
+
Step 2:
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx
index 9de2b72c5..7dcde37b4 100644
--- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/ADCSESC9a.tsx
@@ -19,6 +19,7 @@ import WindowsAbuse from './WindowsAbuse';
import LinuxAbuse from './LinuxAbuse';
import Opsec from './Opsec';
import References from './References';
+import Composition from "./Composition";
const ADCSESC9a = {
general: General,
@@ -26,6 +27,7 @@ const ADCSESC9a = {
linuxAbuse: LinuxAbuse,
opsec: Opsec,
references: References,
+ composition: Composition
};
export default ADCSESC9a;
diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx
new file mode 100644
index 000000000..4a24ff5ea
--- /dev/null
+++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/Composition.tsx
@@ -0,0 +1,54 @@
+// Copyright 2024 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
+
+import { FC } from 'react';
+import { Alert, Box, Skeleton, Typography } from '@mui/material';
+import { apiClient } from '../../../utils/api';
+import { EdgeInfoProps } from '..';
+import { useQuery } from 'react-query';
+import VirtualizedNodeList, { VirtualizedNodeListItem } from '../../VirtualizedNodeList';
+
+const Composition: FC = ({ sourceDBId, targetDBId, edgeName }) => {
+ const { data, isLoading, isError } = useQuery(['edgeComposition', sourceDBId, targetDBId, edgeName], ({ signal }) =>
+ apiClient.getEdgeComposition(sourceDBId!, targetDBId!, edgeName!).then((result) => result.data)
+ );
+
+ const nodesArray: VirtualizedNodeListItem[] = Object.values(data?.data.nodes || {}).map((node) => ({
+ name: node.label,
+ objectId: node.objectId,
+ kind: node.kind,
+ }));
+
+ return (
+ <>
+
+ The relationship represents the effective outcome of the configuration and relationships between several
+ different objects. All objects involved in the creation of this relationship are listed here:
+
+
+ {isLoading ? (
+
+ ) : isError ? (
+ Couldn't load edge composition
+ ) : (
+
+ )}
+
+ >
+ );
+};
+
+export default Composition;