diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index 6406891ffd..d3c4188b44 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -21,8 +21,8 @@ package ad_test import ( "context" - "github.com/specterops/bloodhound/analysis" + ad2 "github.com/specterops/bloodhound/analysis/ad" "github.com/specterops/bloodhound/dawgs/ops" @@ -177,3 +177,89 @@ func TestGoldenCert(t *testing.T) { }) } + +func TestEnrollOnBehalfOf(t *testing.T) { + testContext := integration.NewGraphTestContext(t) + testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) { + harness.EnrollOnBehalfOfHarnessOne.Setup(testContext) + }, func(harness integration.HarnessDetails, db graph.Database) error { + certTemplates, err := ad2.FetchNodesByKind(context.Background(), db, ad.CertTemplate) + v1Templates := make([]*graph.Node, 0) + v2Templates := make([]*graph.Node, 0) + for _, template := range certTemplates { + if version, err := template.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { + continue + } else if version == 1 { + v1Templates = append(v1Templates, template) + } else if version >= 2 { + v2Templates = append(v2Templates, template) + } + } + require.Nil(t, err) + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + results, err := ad2.EnrollOnBehalfOfVersionOne(tx, v1Templates, certTemplates) + require.Nil(t, err) + + require.Len(t, results, 2) + + require.Contains(t, results, analysis.CreatePostRelationshipJob{ + FromID: harness.EnrollOnBehalfOfHarnessOne.CertTemplate11.ID, + ToID: harness.EnrollOnBehalfOfHarnessOne.CertTemplate12.ID, + Kind: ad.EnrollOnBehalfOf, + }) + + require.Contains(t, results, analysis.CreatePostRelationshipJob{ + FromID: harness.EnrollOnBehalfOfHarnessOne.CertTemplate13.ID, + ToID: harness.EnrollOnBehalfOfHarnessOne.CertTemplate12.ID, + Kind: ad.EnrollOnBehalfOf, + }) + + return nil + }) + + return nil + }) + + testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) { + harness.EnrollOnBehalfOfHarnessTwo.Setup(testContext) + }, func(harness integration.HarnessDetails, db graph.Database) error { + certTemplates, err := ad2.FetchNodesByKind(context.Background(), db, ad.CertTemplate) + v1Templates := make([]*graph.Node, 0) + v2Templates := make([]*graph.Node, 0) + for _, template := range certTemplates { + if version, err := template.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { + continue + } else if version == 1 { + v1Templates = append(v1Templates, template) + } else if version >= 2 { + v2Templates = append(v2Templates, template) + } + } + require.Nil(t, err) + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + results, err := ad2.EnrollOnBehalfOfVersionTwo(tx, v2Templates, certTemplates) + require.Nil(t, err) + + require.Len(t, results, 1) + require.Contains(t, results, analysis.CreatePostRelationshipJob{ + FromID: harness.EnrollOnBehalfOfHarnessTwo.CertTemplate21.ID, + ToID: harness.EnrollOnBehalfOfHarnessTwo.CertTemplate23.ID, + Kind: ad.EnrollOnBehalfOf, + }) + + results, err = ad2.EnrollOnBehalfOfSelfControl(tx, v1Templates) + require.Nil(t, err) + + require.Len(t, results, 1) + require.Contains(t, results, analysis.CreatePostRelationshipJob{ + FromID: harness.EnrollOnBehalfOfHarnessTwo.CertTemplate25.ID, + ToID: harness.EnrollOnBehalfOfHarnessTwo.CertTemplate25.ID, + Kind: ad.EnrollOnBehalfOf, + }) + + return nil + }) + + return nil + }) +} diff --git a/cmd/api/src/test/integration/graph.go b/cmd/api/src/test/integration/graph.go index 141bfaefe2..7bbfd8bf3f 100644 --- a/cmd/api/src/test/integration/graph.go +++ b/cmd/api/src/test/integration/graph.go @@ -510,7 +510,7 @@ func (s *GraphTestContext) NewActiveDirectoryRootCA(name, domainSID string) *gra }), ad.Entity, ad.RootCA) } -func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, requiresManagerApproval, authenticationEnabled, enrolleeSupplieSubject bool, schemaVersion, authorizedSignatures int) *graph.Node { +func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, requiresManagerApproval, authenticationEnabled, enrolleeSupplieSubject, subjectAltRequireUpn bool, schemaVersion, authorizedSignatures int, ekus, applicationPolicies []string) *graph.Node { return s.NewNode(graph.AsProperties(graph.PropertyMap{ common.Name: name, common.ObjectID: must.NewUUIDv4().String(), @@ -520,6 +520,9 @@ func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string ad.EnrolleeSuppliesSubject: enrolleeSupplieSubject, ad.SchemaVersion: float64(schemaVersion), ad.AuthorizedSignatures: float64(authorizedSignatures), + ad.EKUs: ekus, + ad.ApplicationPolicies: applicationPolicies, + ad.SubjectAltRequireUPN: subjectAltRequireUpn, }), ad.Entity, ad.CertTemplate) } diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 53d0b52983..8766d89976 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -1134,12 +1134,13 @@ type ADCSESC1Harness struct { } func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { + emptyEkus := make([]string, 0) sid := RandomDomainSID() s.Domain1 = graphTestContext.NewActiveDirectoryDomain("domain 1", sid, false, true) s.AuthStore1 = graphTestContext.NewActiveDirectoryNTAuthStore("ntauthstore 1", sid) s.EnterpriseCA1 = graphTestContext.NewActiveDirectoryEnterpriseCA("eca 1", sid) s.RootCA1 = graphTestContext.NewActiveDirectoryRootCA("rca 1", sid) - s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 1", sid, false, true, true, 1, 0) + s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 1", sid, false, true, true, false, 1, 0, emptyEkus, emptyEkus) s.Group11 = graphTestContext.NewActiveDirectoryGroup("group1-1", sid) s.Group12 = graphTestContext.NewActiveDirectoryGroup("group1-2", sid) s.Group13 = graphTestContext.NewActiveDirectoryGroup("group1-3", sid) @@ -1175,7 +1176,7 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.EnterpriseCA22 = graphTestContext.NewActiveDirectoryEnterpriseCA("eca2-2", sid) s.Group21 = graphTestContext.NewActiveDirectoryGroup("group2-1", sid) s.Group22 = graphTestContext.NewActiveDirectoryGroup("group2-2", sid) - s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 2", sid, false, true, true, 1, 0) + s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 2", sid, false, true, true, false, 1, 0, emptyEkus, emptyEkus) graphTestContext.NewRelationship(s.RootCA2, s.Domain2, ad.RootCAFor) graphTestContext.NewRelationship(s.AuthStore2, s.Domain2, ad.NTAuthStoreFor) @@ -1197,7 +1198,7 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.EnterpriseCA32 = graphTestContext.NewActiveDirectoryEnterpriseCA("eca3-2", sid) s.Group31 = graphTestContext.NewActiveDirectoryGroup("group3-1", sid) s.Group32 = graphTestContext.NewActiveDirectoryGroup("group3-2", sid) - s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 3", sid, false, true, true, 1, 0) + s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 3", sid, false, true, true, false, 1, 0, emptyEkus, emptyEkus) graphTestContext.NewRelationship(s.RootCA3, s.Domain3, ad.RootCAFor) graphTestContext.NewRelationship(s.AuthStore3, s.Domain3, ad.NTAuthStoreFor) @@ -1222,12 +1223,12 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { s.Group44 = graphTestContext.NewActiveDirectoryGroup("group4-4", sid) s.Group45 = graphTestContext.NewActiveDirectoryGroup("group4-5", sid) s.Group46 = graphTestContext.NewActiveDirectoryGroup("group4-6", sid) - s.CertTemplate41 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-1", sid, false, true, true, 2, 1) - s.CertTemplate42 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-2", sid, false, true, true, 2, 0) - s.CertTemplate43 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-3", sid, false, true, true, 1, 0) - s.CertTemplate44 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-4", sid, true, true, true, 1, 0) - s.CertTemplate45 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-5", sid, false, false, true, 1, 0) - s.CertTemplate46 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-6", sid, false, true, false, 1, 0) + s.CertTemplate41 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-1", sid, false, true, true, false, 2, 1, emptyEkus, emptyEkus) + s.CertTemplate42 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-2", sid, false, true, true, false, 2, 0, emptyEkus, emptyEkus) + s.CertTemplate43 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-3", sid, false, true, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate44 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-4", sid, true, true, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate45 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-5", sid, false, false, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate46 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-6", sid, false, true, false, true, 1, 0, emptyEkus, emptyEkus) graphTestContext.NewRelationship(s.AuthStore4, s.Domain4, ad.NTAuthStoreFor) graphTestContext.NewRelationship(s.RootCA4, s.Domain4, ad.RootCAFor) @@ -1251,7 +1252,76 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { graphTestContext.NewRelationship(s.CertTemplate44, s.EnterpriseCA4, ad.PublishedTo) graphTestContext.NewRelationship(s.CertTemplate45, s.EnterpriseCA4, ad.PublishedTo) graphTestContext.NewRelationship(s.CertTemplate46, s.EnterpriseCA4, ad.PublishedTo) +} + +type EnrollOnBehalfOfHarnessTwo struct { + Domain2 *graph.Node + AuthStore2 *graph.Node + RootCA2 *graph.Node + EnterpriseCA2 *graph.Node + CertTemplate21 *graph.Node + CertTemplate22 *graph.Node + CertTemplate23 *graph.Node + CertTemplate24 *graph.Node + CertTemplate25 *graph.Node +} +func (s *EnrollOnBehalfOfHarnessTwo) Setup(gt *GraphTestContext) { + certRequestAgentEKU := make([]string, 0) + certRequestAgentEKU = append(certRequestAgentEKU, adAnalysis.EkuCertRequestAgent) + emptyAppPolicies := make([]string, 0) + sid := RandomDomainSID() + s.Domain2 = gt.NewActiveDirectoryDomain("domain2", sid, false, true) + s.AuthStore2 = gt.NewActiveDirectoryNTAuthStore("authstore2", sid) + s.RootCA2 = gt.NewActiveDirectoryRootCA("rca2", sid) + s.EnterpriseCA2 = gt.NewActiveDirectoryEnterpriseCA("eca2", sid) + s.CertTemplate21 = gt.NewActiveDirectoryCertTemplate("certtemplate2-1", sid, false, false, false, false, 1, 0, certRequestAgentEKU, emptyAppPolicies) + s.CertTemplate22 = gt.NewActiveDirectoryCertTemplate("certtemplate2-2", sid, false, false, false, false, 1, 0, []string{adAnalysis.EkuCertRequestAgent, adAnalysis.EkuAnyPurpose}, emptyAppPolicies) + s.CertTemplate23 = gt.NewActiveDirectoryCertTemplate("certtemplate2-3", sid, false, false, false, false, 2, 1, certRequestAgentEKU, []string{adAnalysis.EkuCertRequestAgent}) + s.CertTemplate24 = gt.NewActiveDirectoryCertTemplate("certtemplate2-4", sid, false, false, false, false, 2, 1, []string{}, emptyAppPolicies) + s.CertTemplate25 = gt.NewActiveDirectoryCertTemplate("certtemplate2-5", sid, false, false, false, true, 1, 1, []string{}, emptyAppPolicies) + + gt.NewRelationship(s.AuthStore2, s.Domain2, ad.NTAuthStoreFor) + gt.NewRelationship(s.RootCA2, s.Domain2, ad.RootCAFor) + gt.NewRelationship(s.EnterpriseCA2, s.AuthStore2, ad.TrustedForNTAuth) + gt.NewRelationship(s.EnterpriseCA2, s.RootCA2, ad.EnterpriseCAFor) + gt.NewRelationship(s.CertTemplate21, s.EnterpriseCA2, ad.PublishedTo) + gt.NewRelationship(s.CertTemplate22, s.EnterpriseCA2, ad.PublishedTo) + gt.NewRelationship(s.CertTemplate23, s.EnterpriseCA2, ad.PublishedTo) + gt.NewRelationship(s.CertTemplate24, s.EnterpriseCA2, ad.PublishedTo) + gt.NewRelationship(s.CertTemplate25, s.EnterpriseCA2, ad.PublishedTo) +} + +type EnrollOnBehalfOfHarnessOne struct { + Domain1 *graph.Node + AuthStore1 *graph.Node + RootCA1 *graph.Node + EnterpriseCA1 *graph.Node + CertTemplate11 *graph.Node + CertTemplate12 *graph.Node + CertTemplate13 *graph.Node +} + +func (s *EnrollOnBehalfOfHarnessOne) Setup(gt *GraphTestContext) { + sid := RandomDomainSID() + anyPurposeEkus := make([]string, 0) + anyPurposeEkus = append(anyPurposeEkus, adAnalysis.EkuAnyPurpose) + emptyAppPolicies := make([]string, 0) + s.Domain1 = gt.NewActiveDirectoryDomain("domain1", sid, false, true) + s.AuthStore1 = gt.NewActiveDirectoryNTAuthStore("authstore1", sid) + s.RootCA1 = gt.NewActiveDirectoryRootCA("rca1", sid) + s.EnterpriseCA1 = gt.NewActiveDirectoryEnterpriseCA("eca1", sid) + s.CertTemplate11 = gt.NewActiveDirectoryCertTemplate("certtemplate1-1", sid, false, false, false, false, 2, 0, anyPurposeEkus, emptyAppPolicies) + s.CertTemplate12 = gt.NewActiveDirectoryCertTemplate("certtemplate1-2", sid, false, false, false, false, 1, 0, anyPurposeEkus, emptyAppPolicies) + s.CertTemplate13 = gt.NewActiveDirectoryCertTemplate("certtemplate1-3", sid, false, false, false, false, 2, 0, anyPurposeEkus, emptyAppPolicies) + + gt.NewRelationship(s.AuthStore1, s.Domain1, ad.NTAuthStoreFor) + gt.NewRelationship(s.RootCA1, s.Domain1, ad.RootCAFor) + gt.NewRelationship(s.EnterpriseCA1, s.AuthStore1, ad.TrustedForNTAuth) + gt.NewRelationship(s.EnterpriseCA1, s.RootCA1, ad.EnterpriseCAFor) + gt.NewRelationship(s.CertTemplate11, s.EnterpriseCA1, ad.PublishedTo) + gt.NewRelationship(s.CertTemplate12, s.EnterpriseCA1, ad.PublishedTo) + gt.NewRelationship(s.CertTemplate13, s.EnterpriseCA1, ad.PublishedTo) } type ADCSGoldenCertHarness struct { @@ -1414,6 +1484,8 @@ type HarnessDetails struct { SearchHarness SearchHarness ShortcutHarness ShortcutHarness ADCSESC1Harness ADCSESC1Harness + EnrollOnBehalfOfHarnessOne EnrollOnBehalfOfHarnessOne + EnrollOnBehalfOfHarnessTwo EnrollOnBehalfOfHarnessTwo ADCSGoldenCertHarness ADCSGoldenCertHarness NumCollectedActiveDirectoryDomains int AZInboundControlHarness AZInboundControlHarness diff --git a/cmd/api/src/test/integration/harnesses/enrollonbehalfof-1.json b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-1.json new file mode 100644 index 0000000000..61d33b1cec --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-1.json @@ -0,0 +1,235 @@ +{ + "style": { + "font-family": "sans-serif", + "background-color": "#ffffff", + "background-image": "", + "background-size": "100%", + "node-color": "#ffffff", + "border-width": 4, + "border-color": "#000000", + "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": "#000000", + "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" + }, + "nodes": [ + { + "id": "n1", + "position": { + "x": 729.9551990267428, + "y": -4 + }, + "caption": "Domain1", + "labels": [], + "properties": {}, + "style": { + "node-color": "#68ccca" + } + }, + { + "id": "n2", + "position": { + "x": 129, + "y": 273.97628342478527 + }, + "caption": "CertTemplate1-1", + "labels": [], + "properties": { + "schemaversion": "2", + "ekus": "[\"2.5.29.37.0\"]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n3", + "position": { + "x": 487.6313891898351, + "y": -4 + }, + "caption": "NTAuthStore1", + "labels": [], + "properties": {}, + "style": { + "node-color": "#7b64ff" + } + }, + { + "id": "n4", + "position": { + "x": 487.6313891898351, + "y": 273.97628342478527 + }, + "caption": "EnterpriseCA1", + "labels": [], + "properties": {}, + "style": { + "node-color": "#b0bc00" + } + }, + { + "id": "n5", + "position": { + "x": 230.03558347937087, + "y": 551.9525668495705 + }, + "caption": "CertTemplate1-2", + "labels": [], + "properties": { + "schemaversion": "1", + "ekus": "[\"2.5.29.37.0\"]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n6", + "position": { + "x": 508.01086036954564, + "y": 551.2045130499298 + }, + "caption": "CertTemplate1-3", + "labels": [], + "properties": { + "schemaversion": "2", + "ekus": "[\"2.5.29.37.0\"]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n7", + "position": { + "x": 729.9551990267428, + "y": 273.97628342478527 + }, + "caption": "RootCA1", + "labels": [], + "properties": {}, + "style": { + "node-color": "#e27300" + } + } + ], + "relationships": [ + { + "id": "n0", + "fromId": "n3", + "toId": "n1", + "type": "NTAuthStoreFor", + "properties": {}, + "style": {} + }, + { + "id": "n1", + "fromId": "n4", + "toId": "n3", + "type": "TrustedForNTAuth", + "properties": {}, + "style": {} + }, + { + "id": "n2", + "fromId": "n2", + "toId": "n4", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n3", + "fromId": "n5", + "toId": "n4", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n4", + "fromId": "n6", + "toId": "n4", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n5", + "fromId": "n7", + "toId": "n1", + "type": "RootCAFor", + "properties": {}, + "style": {} + }, + { + "id": "n6", + "fromId": "n4", + "toId": "n7", + "type": "EnterpriseCAFor", + "properties": {}, + "style": {} + }, + { + "id": "n7", + "fromId": "n2", + "toId": "n5", + "type": "EnrollOnBehalfOf", + "properties": {}, + "style": { + "arrow-color": "#68ccca" + } + }, + { + "id": "n8", + "fromId": "n6", + "toId": "n5", + "type": "EnrollOnBehalfOf", + "properties": {}, + "style": { + "arrow-color": "#68ccca" + } + } + ] +} \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/enrollonbehalfof-1.svg b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-1.svg new file mode 100644 index 0000000000..d075e118df --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-1.svg @@ -0,0 +1 @@ +NTAuthStoreForTrustedForNTAuthPublishedToPublishedToPublishedToRootCAForEnterpriseCAForEnrollOnBehalfOfEnrollOnBehalfOfDomain1CertTemplate1-1schemaversion:2ekus:["2.5.29.37.0"]NTAuthStore1EnterpriseCA1CertTemplate1-2schemaversion:1ekus:["2.5.29.37.0"]CertTemplate1-3schemaversion:2ekus:["2.5.29.37.0"]RootCA1 \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/enrollonbehalfof-2.json b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-2.json new file mode 100644 index 0000000000..5ea093bf1c --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-2.json @@ -0,0 +1,287 @@ +{ + "nodes": [ + { + "id": "n0", + "position": { + "x": -569.1685177598522, + "y": -1021.0927494329366 + }, + "caption": "Domain2", + "labels": [], + "properties": {}, + "style": { + "node-color": "#68ccca" + } + }, + { + "id": "n2", + "position": { + "x": -811.4923275967599, + "y": -1021.0927494329366 + }, + "caption": "NTAuthStore2", + "labels": [], + "properties": {}, + "style": { + "node-color": "#7b64ff" + } + }, + { + "id": "n5", + "position": { + "x": -811.4923275967599, + "y": -743.1164660081513 + }, + "caption": "EnterpriseCA2", + "labels": [], + "properties": {}, + "style": { + "node-color": "#b0bc00" + } + }, + { + "id": "n13", + "position": { + "x": -569.1685177598522, + "y": -743.1164660081513 + }, + "caption": "RootCA2", + "labels": [], + "properties": {}, + "style": { + "node-color": "#e27300" + } + }, + { + "id": "n14", + "position": { + "x": -1151.5140057279425, + "y": -743.1164660081513 + }, + "caption": "CertTemplate2-1", + "labels": [], + "properties": { + "ekus": "[\"1.3.6.1.4.1.311.20.2.1\"]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n15", + "position": { + "x": -1151.5140057279425, + "y": -546.8048586088046 + }, + "caption": "CertTemplate2-2", + "labels": [], + "properties": { + "ekus": "[\"1.3.6.1.4.1.311.20.2.1\", \"2.5.29.37.0\"]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n16", + "position": { + "x": -981.5031666623508, + "y": -448.6490549091318 + }, + "caption": "CertTemplate2-3", + "labels": [], + "properties": { + "ekus": "[]", + "schemaversion": "2", + "authorizedsignatures": "1", + "applicationpolicies": "[\"1.3.6.1.4.1.311.20.2.1\"]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n17", + "position": { + "x": -695.4923275967599, + "y": -448.6490549091318 + }, + "caption": "CertTemplate2-4", + "labels": [], + "properties": { + "ekus": "[]", + "schemaversion": "2", + "authorizedsignatures": "1", + "applicationpolicies": "[]" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n18", + "position": { + "x": -517.02491649774, + "y": -448.6490549091315 + }, + "caption": "CertTemplate2-5", + "labels": [], + "properties": { + "ekus": "[]", + "schemaversion": "1", + "subjectaltrequiresupn": "true" + }, + "style": { + "node-color": "#fda1ff" + } + } + ], + "relationships": [ + { + "id": "n0", + "fromId": "n2", + "toId": "n0", + "type": "NTAuthStoreFor", + "properties": {}, + "style": {} + }, + { + "id": "n1", + "fromId": "n5", + "toId": "n2", + "type": "TrustedForNTAuth", + "properties": {}, + "style": {} + }, + { + "id": "n9", + "fromId": "n13", + "toId": "n0", + "type": "RootCAFor", + "properties": {}, + "style": {} + }, + { + "id": "n10", + "fromId": "n5", + "toId": "n13", + "type": "EnterpriseCAFor", + "properties": {}, + "style": {} + }, + { + "id": "n11", + "fromId": "n14", + "toId": "n5", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n12", + "fromId": "n15", + "toId": "n5", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n13", + "fromId": "n16", + "toId": "n5", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n14", + "fromId": "n17", + "toId": "n5", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n15", + "type": "EnrollOnBehalfOf", + "style": { + "arrow-color": "#a4dd00" + }, + "properties": {}, + "toId": "n16", + "fromId": "n14" + }, + { + "id": "n16", + "fromId": "n18", + "toId": "n5", + "type": "PublishedTo", + "properties": {}, + "style": {} + }, + { + "id": "n17", + "fromId": "n18", + "toId": "n18", + "type": "EnrollOnBehalfOf", + "properties": {}, + "style": { + "type-color": "#4d4d4d", + "arrow-color": "#a4dd00" + } + } + ], + "style": { + "font-family": "sans-serif", + "background-color": "#ffffff", + "background-image": "", + "background-size": "100%", + "node-color": "#ffffff", + "border-width": 4, + "border-color": "#000000", + "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": "#000000", + "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" + } +} \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/enrollonbehalfof-2.svg b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-2.svg new file mode 100644 index 0000000000..75b89d04f0 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/enrollonbehalfof-2.svg @@ -0,0 +1 @@ +NTAuthStoreForTrustedForNTAuthRootCAForEnterpriseCAForPublishedToPublishedToPublishedToPublishedToEnrollOnBehalfOfPublishedToEnrollOnBehalfOfDomain2NTAuthStore2EnterpriseCA2RootCA2CertTemplate2-1ekus:["1.3.6.1.4.1.311.20.2.1"]CertTemplate2-2ekus:["1.3.6.1.4.1.311.20.2.1", "2.5.29.37.0"]CertTemplate2-3ekus:[]schemaversion:2authorizedsignatures:1applicationpolicies:["1.3.6.1.4.1.311.20.2.1"]CertTemplate2-4ekus:[]schemaversion:2authorizedsignatures:1applicationpolicies:[]CertTemplate2-5ekus:[]schemaversion:1subjectaltrequiresupn:true \ No newline at end of file diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index 063a95af04..6aad2beb57 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -154,12 +154,12 @@ func validatePublishedCertTemplateForEsc1(properties PublishedCertTemplateValida } } -func PostEnrollOnBehalfOf(certTemplates []graph.Node, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { - versionOneTemplates := make([]graph.Node, 0) - versionTwoTemplates := make([]graph.Node, 0) +func PostEnrollOnBehalfOf(certTemplates []*graph.Node, operation analysis.StatTrackedOperation[analysis.CreatePostRelationshipJob]) error { + versionOneTemplates := make([]*graph.Node, 0) + versionTwoTemplates := make([]*graph.Node, 0) for _, node := range certTemplates { - if version, err := node.Properties.Get(ad.SchemaVersion.String()).Int(); err != nil { + if version, err := node.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { log.Errorf("Error getting schema version for cert template %d: %v", node.ID, err) } else { if version == 1 { @@ -173,21 +173,52 @@ func PostEnrollOnBehalfOf(certTemplates []graph.Node, operation analysis.StatTra } operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - return enrollOnBehalfOfVersionTwo(tx, versionTwoTemplates, certTemplates, outC) + if results, err := EnrollOnBehalfOfVersionTwo(tx, versionTwoTemplates, certTemplates); err != nil { + return err + } else { + for _, result := range results { + if !channels.Submit(ctx, outC, result) { + return nil + } + } + + return nil + } }) operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - return enrollOnBehalfOfVersionOne(tx, versionOneTemplates, certTemplates, outC) + if results, err := EnrollOnBehalfOfVersionOne(tx, versionOneTemplates, certTemplates); err != nil { + return err + } else { + for _, result := range results { + if !channels.Submit(ctx, outC, result) { + return nil + } + } + + return nil + } }) operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { - return enrollOnBehalfOfSelfControl(tx, versionOneTemplates, outC) + if results, err := EnrollOnBehalfOfSelfControl(tx, versionOneTemplates); err != nil { + return err + } else { + for _, result := range results { + if !channels.Submit(ctx, outC, result) { + return nil + } + } + + return nil + } }) return nil } -func enrollOnBehalfOfVersionTwo(tx graph.Transaction, versionTwoCertTemplates, allCertTemplates []graph.Node, outC chan<- analysis.CreatePostRelationshipJob) error { +func EnrollOnBehalfOfVersionTwo(tx graph.Transaction, versionTwoCertTemplates, allCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { + results := make([]analysis.CreatePostRelationshipJob, 0) for _, certTemplateOne := range allCertTemplates { if hasBadEku, err := certTemplateHasEku(certTemplateOne, EkuAnyPurpose); err != nil { log.Errorf("error getting ekus for cert template %d: %w", certTemplateOne.ID, err) @@ -205,7 +236,9 @@ func enrollOnBehalfOfVersionTwo(tx graph.Transaction, versionTwoCertTemplates, a continue } else { for _, certTemplateTwo := range versionTwoCertTemplates { - if authorizedSignatures, err := certTemplateTwo.Properties.Get(ad.AuthorizedSignatures.String()).Int(); err != nil { + if certTemplateOne.ID == certTemplateTwo.ID { + continue + } else if authorizedSignatures, err := certTemplateTwo.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil { log.Errorf("Error getting authorized signatures for cert template %d: %w", certTemplateTwo.ID, err) } else if authorizedSignatures < 1 { continue @@ -218,23 +251,27 @@ func enrollOnBehalfOfVersionTwo(tx graph.Transaction, versionTwoCertTemplates, a } else if !isLinked { continue } else { - outC <- analysis.CreatePostRelationshipJob{ + results = append(results, analysis.CreatePostRelationshipJob{ FromID: certTemplateOne.ID, ToID: certTemplateTwo.ID, Kind: ad.EnrollOnBehalfOf, - } + }) } } } } - return nil + return results, nil } -func enrollOnBehalfOfVersionOne(tx graph.Transaction, versionOneCertTemplates []graph.Node, allCertTemplates []graph.Node, outC chan<- analysis.CreatePostRelationshipJob) error { +func EnrollOnBehalfOfVersionOne(tx graph.Transaction, versionOneCertTemplates []*graph.Node, allCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { + results := make([]analysis.CreatePostRelationshipJob, 0) + for _, certTemplateOne := range allCertTemplates { //prefilter as much as we can first - if hasEku, err := certTemplateHasEkuOrAll(certTemplateOne, EkuCertRequestAgent, EkuAnyPurpose); err != nil { + if slices.Contains(versionOneCertTemplates, certTemplateOne) { + continue + } else if hasEku, err := certTemplateHasEkuOrAll(certTemplateOne, EkuCertRequestAgent, EkuAnyPurpose); err != nil { log.Errorf("Error checking ekus for certtemplate %d: %w", certTemplateOne.ID, err) } else if !hasEku { continue @@ -253,30 +290,31 @@ func enrollOnBehalfOfVersionOne(tx graph.Transaction, versionOneCertTemplates [] } else if !hasPath { continue } else { - outC <- analysis.CreatePostRelationshipJob{ - FromID: 0, - ToID: 0, - Kind: nil, - } + results = append(results, analysis.CreatePostRelationshipJob{ + FromID: certTemplateOne.ID, + ToID: certTemplateTwo.ID, + Kind: ad.EnrollOnBehalfOf, + }) } } } } - return nil + return results, nil } -func getDomainForCertTemplate(tx graph.Transaction, certTemplate graph.Node) (graph.Node, error) { +func getDomainForCertTemplate(tx graph.Transaction, certTemplate *graph.Node) (*graph.Node, error) { if domainSid, err := certTemplate.Properties.Get(ad.DomainSID.String()).String(); err != nil { - return graph.Node{}, err + return &graph.Node{}, err } else if domainNode, err := analysis.FetchNodeByObjectID(tx, domainSid); err != nil { - return graph.Node{}, err + return &graph.Node{}, err } else { - return *domainNode, nil + return domainNode, nil } } -func enrollOnBehalfOfSelfControl(tx graph.Transaction, versionOneCertTemplates []graph.Node, outC chan<- analysis.CreatePostRelationshipJob) error { +func EnrollOnBehalfOfSelfControl(tx graph.Transaction, versionOneCertTemplates []*graph.Node) ([]analysis.CreatePostRelationshipJob, error) { + results := make([]analysis.CreatePostRelationshipJob, 0) for _, certTemplate := range versionOneCertTemplates { if hasEku, err := certTemplateHasEkuOrAll(certTemplate, EkuAnyPurpose); err != nil { log.Errorf("Error checking ekus for certtemplate %d: %w", certTemplate.ID, err) @@ -293,18 +331,18 @@ func enrollOnBehalfOfSelfControl(tx graph.Transaction, versionOneCertTemplates [ } else if !doesLink { continue } else { - outC <- analysis.CreatePostRelationshipJob{ + results = append(results, analysis.CreatePostRelationshipJob{ FromID: certTemplate.ID, ToID: certTemplate.ID, Kind: ad.EnrollOnBehalfOf, - } + }) } } - return nil + return results, nil } -func certTemplateHasEkuOrAll(certTemplate graph.Node, targetEkus ...string) (bool, error) { +func certTemplateHasEkuOrAll(certTemplate *graph.Node, targetEkus ...string) (bool, error) { if ekus, err := certTemplate.Properties.Get(ad.EKUs.String()).StringSlice(); err != nil { return false, err } else if len(ekus) == 0 { @@ -322,7 +360,7 @@ func certTemplateHasEkuOrAll(certTemplate graph.Node, targetEkus ...string) (boo } } -func certTemplateHasEku(certTemplate graph.Node, targetEkus ...string) (bool, error) { +func certTemplateHasEku(certTemplate *graph.Node, targetEkus ...string) (bool, error) { if ekus, err := certTemplate.Properties.Get(ad.EKUs.String()).StringSlice(); err != nil { return false, err } else { @@ -349,7 +387,7 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching cert template nodes: %w", err) } else if domains, err := FetchNodesByKind(ctx, db, ad.Domain); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed fetching domain nodes: %w", err) - } else if preProcessStats, err := postADCSPreProcess(ctx, db, enterpriseCertAuthorities, rootCertAuthorities); err != nil { + } else if preProcessStats, err := postADCSPreProcess(ctx, db, enterpriseCertAuthorities, rootCertAuthorities, certTemplates); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed adcs pre-processing: %w", err) } else { operation.Stats.Merge(preProcessStats) @@ -386,7 +424,7 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat } } -func postADCSPreProcess(ctx context.Context, db graph.Database, enterpriseCertAuthorities, rootCertAuthorities []*graph.Node) (*analysis.AtomicPostProcessingStats, error) { +func postADCSPreProcess(ctx context.Context, db graph.Database, enterpriseCertAuthorities, rootCertAuthorities, certTemplates []*graph.Node) (*analysis.AtomicPostProcessingStats, error) { operation := analysis.NewPostRelationshipOperation(ctx, db, "ADCS Post Processing - No Dependencies") if err := PostTrustedForNTAuth(ctx, db, operation); err != nil { @@ -395,6 +433,8 @@ func postADCSPreProcess(ctx context.Context, db graph.Database, enterpriseCertAu return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.IssuedSignedBy.String(), err) } else if err := PostEnterpriseCAFor(ctx, db, operation, enterpriseCertAuthorities); err != nil { return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.EnterpriseCAFor.String(), err) + } else if err := PostEnrollOnBehalfOf(certTemplates, operation); err != nil { + return &analysis.AtomicPostProcessingStats{}, fmt.Errorf("failed post processing for %s: %w", ad.EnrollOnBehalfOf.String(), err) } else { return &operation.Stats, operation.Done() } diff --git a/packages/go/analysis/ad/queries.go b/packages/go/analysis/ad/queries.go index c093fe28a7..5c5933cd8f 100644 --- a/packages/go/analysis/ad/queries.go +++ b/packages/go/analysis/ad/queries.go @@ -1439,7 +1439,7 @@ func FetchEnterpriseCAsTrustedForNTAuthPathToDomain(tx graph.Transaction, domain }) } -func DoesCertTemplateLinkToDomain(tx graph.Transaction, certTemplate graph.Node, domainNode graph.Node) (bool, error) { +func DoesCertTemplateLinkToDomain(tx graph.Transaction, certTemplate, domainNode *graph.Node) (bool, error) { if pathSet, err := FetchCertTemplatePathToDomain(tx, certTemplate, domainNode); err != nil { return false, err } else { @@ -1447,7 +1447,7 @@ func DoesCertTemplateLinkToDomain(tx graph.Transaction, certTemplate graph.Node, } } -func FetchCertTemplatePathToDomain(tx graph.Transaction, certTemplate graph.Node, domain graph.Node) (graph.PathSet, error) { +func FetchCertTemplatePathToDomain(tx graph.Transaction, certTemplate, domain *graph.Node) (graph.PathSet, error) { var ( paths = graph.NewPathSet() )