From f099e378889cb21e9957d66fe0fab51d72d6e2ea Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Fri, 26 Jan 2024 10:42:52 -0500 Subject: [PATCH 1/3] ESC9A Post Processing (#340) * feat: esc9a post * test: add esc9 test * chore: add harness files * fix: regen schema after merge * chore: fix small nits * chore: cleanup cert template new function * chore: add missing props * fix: treat failure to grab properties as true * chore: fix typo --- .../src/analysis/ad/adcs_integration_test.go | 63 ++- cmd/api/src/test/integration/graph.go | 44 +- cmd/api/src/test/integration/harnesses.go | 375 ++++++++++++++++-- .../integration/harnesses/esc9aharness.json | 314 +++++++++++++++ .../integration/harnesses/esc9aharness.svg | 18 + packages/go/analysis/ad/adcs.go | 8 + packages/go/analysis/ad/esc9.go | 175 ++++++++ packages/go/graphschema/azure/azure.go | 2 +- packages/go/graphschema/common/common.go | 2 +- packages/go/graphschema/graph.go | 2 +- 10 files changed, 960 insertions(+), 43 deletions(-) create mode 100644 cmd/api/src/test/integration/harnesses/esc9aharness.json create mode 100644 cmd/api/src/test/integration/harnesses/esc9aharness.svg create mode 100644 packages/go/analysis/ad/esc9.go diff --git a/cmd/api/src/analysis/ad/adcs_integration_test.go b/cmd/api/src/analysis/ad/adcs_integration_test.go index 3d48ed75ce..bda73f3ce8 100644 --- a/cmd/api/src/analysis/ad/adcs_integration_test.go +++ b/cmd/api/src/analysis/ad/adcs_integration_test.go @@ -489,7 +489,7 @@ func TestADCSESC3(t *testing.T) { for _, enterpriseCA := range enterpriseCAs { if cache.DoesCAChainProperlyToDomain(enterpriseCA, innerDomain) { if err := ad2.PostADCSESC3(ctx, tx, outC, groupExpansions, enterpriseCA, innerDomain, cache); err != nil { - t.Logf("failed post processing for %s: %v", ad.ADCSESC1.String(), err) + t.Logf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) } else { return nil } @@ -546,7 +546,7 @@ func TestADCSESC3(t *testing.T) { for _, enterpriseCA := range enterpriseCAs { if cache.DoesCAChainProperlyToDomain(enterpriseCA, innerDomain) { if err := ad2.PostADCSESC3(ctx, tx, outC, groupExpansions, enterpriseCA, innerDomain, cache); err != nil { - t.Logf("failed post processing for %s: %v", ad.ADCSESC1.String(), err) + t.Logf("failed post processing for %s: %v", ad.ADCSESC3.String(), err) } else { return nil } @@ -584,6 +584,65 @@ func TestADCSESC3(t *testing.T) { }) } +func TestADCSESC9a(t *testing.T) { + testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) + testContext.DatabaseTestWithSetup(func(harness *integration.HarnessDetails) error { + harness.ESC9AHarness.Setup(testContext) + return nil + }, func(harness integration.HarnessDetails, db graph.Database) { + operation := analysis.NewPostRelationshipOperation(context.Background(), db, "ADCS Post Process Test - ESC9a") + + groupExpansions, err := ad2.ExpandAllRDPLocalGroups(context.Background(), db) + require.Nil(t, err) + enterpriseCertAuthorities, err := ad2.FetchNodesByKind(context.Background(), db, ad.EnterpriseCA) + require.Nil(t, err) + certTemplates, err := ad2.FetchNodesByKind(context.Background(), db, ad.CertTemplate) + require.Nil(t, err) + domains, err := ad2.FetchNodesByKind(context.Background(), db, ad.Domain) + require.Nil(t, err) + + cache := ad2.NewADCSCache() + cache.BuildCache(context.Background(), db, enterpriseCertAuthorities, certTemplates) + + for _, domain := range domains { + innerDomain := domain + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if enterpriseCAs, err := ad2.FetchEnterpriseCAsTrustedForNTAuthToDomain(tx, innerDomain); err != nil { + return err + } else { + for _, enterpriseCA := range enterpriseCAs { + if cache.DoesCAChainProperlyToDomain(enterpriseCA, innerDomain) { + if err := ad2.PostADCSESC9a(ctx, tx, outC, groupExpansions, enterpriseCA, innerDomain, cache); err != nil { + t.Logf("failed post processing for %s: %v", ad.ADCSESC9a.String(), err) + } else { + return nil + } + } + } + } + return nil + }) + } + operation.Done() + + db.ReadTransaction(context.Background(), func(tx graph.Transaction) error { + if results, err := ops.FetchStartNodes(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)) + + require.True(t, results.Contains(harness.ESC9AHarness.Attacker)) + + } + return nil + }) + }) + +} + func TestADCSESC6a(t *testing.T) { testContext := integration.NewGraphTestContext(t, graphschema.DefaultGraphSchema()) diff --git a/cmd/api/src/test/integration/graph.go b/cmd/api/src/test/integration/graph.go index 1effa5d13d..b1459ba1cf 100644 --- a/cmd/api/src/test/integration/graph.go +++ b/cmd/api/src/test/integration/graph.go @@ -414,23 +414,41 @@ func (s *GraphTestContext) NewActiveDirectoryRootCAWithThumbprint(name, domainSI }), ad.Entity, ad.RootCA) } -func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, requiresManagerApproval, authenticationEnabled, enrolleeSupplieSubject, subjectAltRequireUpn, noSecurityExtension bool, schemaVersion, authorizedSignatures int, ekus, applicationPolicies []string) *graph.Node { +func (s *GraphTestContext) NewActiveDirectoryCertTemplate(name, domainSID string, data CertTemplateData) *graph.Node { return s.NewNode(graph.AsProperties(graph.PropertyMap{ - common.Name: name, - common.ObjectID: must.NewUUIDv4().String(), - ad.DomainSID: domainSID, - ad.RequiresManagerApproval: requiresManagerApproval, - ad.AuthenticationEnabled: authenticationEnabled, - ad.EnrolleeSuppliesSubject: enrolleeSupplieSubject, - ad.NoSecurityExtension: noSecurityExtension, - ad.SchemaVersion: float64(schemaVersion), - ad.AuthorizedSignatures: float64(authorizedSignatures), - ad.EKUs: ekus, - ad.ApplicationPolicies: applicationPolicies, - ad.SubjectAltRequireUPN: subjectAltRequireUpn, + common.Name: name, + common.ObjectID: must.NewUUIDv4().String(), + ad.DomainSID: domainSID, + ad.RequiresManagerApproval: data.RequiresManagerApproval, + ad.AuthenticationEnabled: data.AuthenticationEnabled, + ad.EnrolleeSuppliesSubject: data.EnrolleeSuppliesSubject, + ad.NoSecurityExtension: data.NoSecurityExtension, + ad.SchemaVersion: data.SchemaVersion, + ad.AuthorizedSignatures: data.AuthorizedSignatures, + ad.EKUs: data.EKUS, + ad.ApplicationPolicies: data.ApplicationPolicies, + ad.SubjectAltRequireUPN: data.SubjectAltRequireUPN, + ad.SubjectAltRequireSPN: data.SubjectAltRequireSPN, + ad.SubjectAltRequireDNS: data.SubjectAltRequireDNS, + ad.SubjectAltRequireDomainDNS: data.SubjectAltRequireDomainDNS, }), ad.Entity, ad.CertTemplate) } +type CertTemplateData struct { + RequiresManagerApproval bool + AuthenticationEnabled bool + EnrolleeSuppliesSubject bool + SubjectAltRequireUPN bool + SubjectAltRequireSPN bool + SubjectAltRequireDNS bool + SubjectAltRequireDomainDNS bool + NoSecurityExtension bool + SchemaVersion float64 + AuthorizedSignatures float64 + EKUS []string + ApplicationPolicies []string +} + func (s *GraphTestContext) setupAzure() { s.Harness.AZBaseHarness.Setup(s) s.Harness.AZGroupMembership.Setup(s) diff --git a/cmd/api/src/test/integration/harnesses.go b/cmd/api/src/test/integration/harnesses.go index 2138a27932..9b11946519 100644 --- a/cmd/api/src/test/integration/harnesses.go +++ b/cmd/api/src/test/integration/harnesses.go @@ -1144,7 +1144,18 @@ func (s *ADCSESC1Harness) Setup(graphTestContext *GraphTestContext) { 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, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.Group11 = graphTestContext.NewActiveDirectoryGroup("group1-1", sid) s.Group12 = graphTestContext.NewActiveDirectoryGroup("group1-2", sid) s.Group13 = graphTestContext.NewActiveDirectoryGroup("group1-3", sid) @@ -1180,7 +1191,18 @@ 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, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) graphTestContext.NewRelationship(s.RootCA2, s.Domain2, ad.RootCAFor) graphTestContext.NewRelationship(s.AuthStore2, s.Domain2, ad.NTAuthStoreFor) @@ -1202,7 +1224,18 @@ 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, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) graphTestContext.NewRelationship(s.RootCA3, s.Domain3, ad.RootCAFor) graphTestContext.NewRelationship(s.AuthStore3, s.Domain3, ad.NTAuthStoreFor) @@ -1227,12 +1260,78 @@ 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, false, false, 2, 1, emptyEkus, emptyEkus) - s.CertTemplate42 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-2", sid, false, true, true, false, false, 2, 0, emptyEkus, emptyEkus) - s.CertTemplate43 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-3", sid, false, true, true, false, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate44 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-4", sid, true, true, true, false, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate45 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-5", sid, false, false, true, false, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate46 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-6", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate41 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 1, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate42 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate43 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate44 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-4", sid, CertTemplateData{ + RequiresManagerApproval: true, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate45 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-5", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: true, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate46 = graphTestContext.NewActiveDirectoryCertTemplate("certtemplate 4-6", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) graphTestContext.NewRelationship(s.AuthStore4, s.Domain4, ad.NTAuthStoreFor) graphTestContext.NewRelationship(s.RootCA4, s.Domain4, ad.RootCAFor) @@ -1279,11 +1378,66 @@ func (s *EnrollOnBehalfOfHarnessTwo) Setup(gt *GraphTestContext) { 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, false, 1, 0, certRequestAgentEKU, emptyAppPolicies) - s.CertTemplate22 = gt.NewActiveDirectoryCertTemplate("certtemplate2-2", sid, false, false, false, false, false, 1, 0, []string{adAnalysis.EkuCertRequestAgent, adAnalysis.EkuAnyPurpose}, emptyAppPolicies) - s.CertTemplate23 = gt.NewActiveDirectoryCertTemplate("certtemplate2-3", sid, false, false, false, false, false, 2, 1, certRequestAgentEKU, []string{adAnalysis.EkuCertRequestAgent}) - s.CertTemplate24 = gt.NewActiveDirectoryCertTemplate("certtemplate2-4", sid, false, false, false, false, false, 2, 1, []string{}, emptyAppPolicies) - s.CertTemplate25 = gt.NewActiveDirectoryCertTemplate("certtemplate2-5", sid, false, false, false, true, false, 1, 1, []string{}, emptyAppPolicies) + s.CertTemplate21 = gt.NewActiveDirectoryCertTemplate("certtemplate2-1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: certRequestAgentEKU, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate22 = gt.NewActiveDirectoryCertTemplate("certtemplate2-2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: []string{adAnalysis.EkuCertRequestAgent, adAnalysis.EkuAnyPurpose}, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate23 = gt.NewActiveDirectoryCertTemplate("certtemplate2-3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 1, + EKUS: certRequestAgentEKU, + ApplicationPolicies: []string{adAnalysis.EkuCertRequestAgent}, + }) + s.CertTemplate24 = gt.NewActiveDirectoryCertTemplate("certtemplate2-4", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 1, + EKUS: emptyAppPolicies, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate25 = gt.NewActiveDirectoryCertTemplate("certtemplate2-5", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 1, + EKUS: emptyAppPolicies, + ApplicationPolicies: emptyAppPolicies, + }) gt.NewRelationship(s.AuthStore2, s.Domain2, ad.NTAuthStoreFor) gt.NewRelationship(s.RootCA2, s.Domain2, ad.RootCAFor) @@ -1315,9 +1469,42 @@ func (s *EnrollOnBehalfOfHarnessOne) Setup(gt *GraphTestContext) { 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, false, 2, 0, anyPurposeEkus, emptyAppPolicies) - s.CertTemplate12 = gt.NewActiveDirectoryCertTemplate("certtemplate1-2", sid, false, false, false, false, false, 1, 0, anyPurposeEkus, emptyAppPolicies) - s.CertTemplate13 = gt.NewActiveDirectoryCertTemplate("certtemplate1-3", sid, false, false, false, false, false, 2, 0, anyPurposeEkus, emptyAppPolicies) + s.CertTemplate11 = gt.NewActiveDirectoryCertTemplate("certtemplate1-1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: anyPurposeEkus, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate12 = gt.NewActiveDirectoryCertTemplate("certtemplate1-2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: anyPurposeEkus, + ApplicationPolicies: emptyAppPolicies, + }) + s.CertTemplate13 = gt.NewActiveDirectoryCertTemplate("certtemplate1-3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: anyPurposeEkus, + ApplicationPolicies: emptyAppPolicies, + }) gt.NewRelationship(s.AuthStore1, s.Domain1, ad.NTAuthStoreFor) gt.NewRelationship(s.RootCA1, s.Domain1, ad.RootCAFor) @@ -1558,10 +1745,54 @@ func (s *ESC3Harness1) Setup(graphTestContext *GraphTestContext) { s.User3 = graphTestContext.NewActiveDirectoryUser("User3", sid) s.Group1 = graphTestContext.NewActiveDirectoryGroup("Group1", sid) s.Group2 = graphTestContext.NewActiveDirectoryGroup("Group2", sid) - s.CertTemplate0 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate0", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate1", sid, false, false, false, false, false, 2, 0, emptyEkus, emptyEkus) - s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate2", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) - s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate3", sid, false, false, false, false, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate0 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate0", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate1 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate2 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate3 = graphTestContext.NewActiveDirectoryCertTemplate("CertTemplate3", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: false, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.EnterpriseCA1 = graphTestContext.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", sid) s.EnterpriseCA2 = graphTestContext.NewActiveDirectoryEnterpriseCA("EnterpriseCA2", sid) s.NTAuthStore = graphTestContext.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) @@ -1616,8 +1847,30 @@ func (s *ESC3Harness2) Setup(c *GraphTestContext) { s.User1 = c.NewActiveDirectoryUser("User1", sid) s.User2 = c.NewActiveDirectoryUser("User2", sid) s.Group1 = c.NewActiveDirectoryGroup("Group1", sid) - s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, false, true, false, false, false, 2, 0, emptyEkus, emptyEkus) - s.CertTemplate2 = c.NewActiveDirectoryCertTemplate("CertTemplate2", sid, false, true, false, true, false, 1, 0, emptyEkus, emptyEkus) + s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 2, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.CertTemplate2 = c.NewActiveDirectoryCertTemplate("CertTemplate2", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: false, + NoSecurityExtension: false, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) s.EnterpriseCA1 = c.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", sid) s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) @@ -1642,6 +1895,53 @@ func (s *ESC3Harness2) Setup(c *GraphTestContext) { c.UpdateNode(s.EnterpriseCA1) } +type ESC9AHarness struct { + Domain *graph.Node + NTAuthStore *graph.Node + RootCA *graph.Node + DC *graph.Node + EnterpriseCA *graph.Node + CertTemplate *graph.Node + Victim *graph.Node + Attacker *graph.Node +} + +func (s *ESC9AHarness) Setup(c *GraphTestContext) { + sid := RandomDomainSID() + emptyEkus := make([]string, 0) + s.Domain = c.NewActiveDirectoryDomain("ESC9aDomain", sid, false, true) + s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) + s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) + s.DC = c.NewActiveDirectoryComputer("DC", sid) + s.EnterpriseCA = c.NewActiveDirectoryEnterpriseCA("eca", sid) + s.CertTemplate = c.NewActiveDirectoryCertTemplate("certtemplate", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: true, + SubjectAltRequireSPN: true, + SubjectAltRequireDNS: false, + NoSecurityExtension: true, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: emptyEkus, + ApplicationPolicies: emptyEkus, + }) + s.Victim = c.NewActiveDirectoryUser("victim", sid, false) + s.Attacker = c.NewActiveDirectoryUser("attacker", sid, false) + + c.NewRelationship(s.DC, s.Domain, ad.DCFor) + c.NewRelationship(s.NTAuthStore, s.Domain, ad.NTAuthStoreFor) + c.NewRelationship(s.RootCA, s.Domain, ad.RootCAFor) + c.NewRelationship(s.EnterpriseCA, s.DC, ad.CanAbuseWeakCertBinding) + c.NewRelationship(s.EnterpriseCA, s.NTAuthStore, ad.TrustedForNTAuth) + c.NewRelationship(s.EnterpriseCA, s.RootCA, ad.IssuedSignedBy) + c.NewRelationship(s.CertTemplate, s.EnterpriseCA, ad.PublishedTo) + c.NewRelationship(s.Victim, s.EnterpriseCA, ad.Enroll) + c.NewRelationship(s.Victim, s.CertTemplate, ad.Enroll) + c.NewRelationship(s.Attacker, s.Victim, ad.GenericWrite) +} + type ESC6aHarnessPrincipalEdges struct { Group0 *graph.Node Group1 *graph.Node @@ -1665,7 +1965,18 @@ func (s *ESC6aHarnessPrincipalEdges) Setup(c *GraphTestContext) { s.Group2 = c.NewActiveDirectoryGroup("Group2", sid) s.Group3 = c.NewActiveDirectoryGroup("Group3", sid) s.Group4 = c.NewActiveDirectoryGroup("Group4", sid) - s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, false, true, false, false, true, 1, 0, []string{}, []string{}) + s.CertTemplate1 = c.NewActiveDirectoryCertTemplate("CertTemplate1", sid, CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: true, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: []string{}, + ApplicationPolicies: []string{}, + }) s.EnterpriseCA1 = c.NewActiveDirectoryEnterpriseCA("EnterpriseCA1", sid) s.NTAuthStore = c.NewActiveDirectoryNTAuthStore("NTAuthStore", sid) s.RootCA = c.NewActiveDirectoryRootCA("RootCA", sid) @@ -1712,6 +2023,19 @@ func setupHarnessFromArrowsJson(c *GraphTestContext, fileName string) { func initHarnessNodes(c *GraphTestContext, nodes []harnesses.Node, sid string) map[string]*graph.Node { nodeMap := map[string]*graph.Node{} + ctData := CertTemplateData{ + RequiresManagerApproval: false, + AuthenticationEnabled: true, + EnrolleeSuppliesSubject: false, + SubjectAltRequireUPN: false, + SubjectAltRequireSPN: false, + NoSecurityExtension: true, + SchemaVersion: 1, + AuthorizedSignatures: 0, + EKUS: []string{}, + ApplicationPolicies: []string{}, + } + for _, node := range nodes { if kind, ok := node.Properties["kind"]; !ok { continue @@ -1721,7 +2045,7 @@ func initHarnessNodes(c *GraphTestContext, nodes []harnesses.Node, sid string) m case ad.Group.String(): nodeMap[node.ID] = c.NewActiveDirectoryGroup(node.Caption, sid) case ad.CertTemplate.String(): - nodeMap[node.ID] = c.NewActiveDirectoryCertTemplate(node.Caption, sid, false, true, false, false, true, 1, 0, []string{}, []string{}) + nodeMap[node.ID] = c.NewActiveDirectoryCertTemplate(node.Caption, sid, ctData) case ad.EnterpriseCA.String(): nodeMap[node.ID] = c.NewActiveDirectoryEnterpriseCA(node.Caption, sid) case ad.NTAuthStore.String(): @@ -1900,4 +2224,5 @@ type HarnessDetails struct { ESC6aHarnessECA ESC6aHarnessECA ESC6aHarnessTemplate1 ESC6aHarnessTemplate1 ESC6aHarnessTemplate2 ESC6aHarnessTemplate2 + ESC9AHarness ESC9AHarness } diff --git a/cmd/api/src/test/integration/harnesses/esc9aharness.json b/cmd/api/src/test/integration/harnesses/esc9aharness.json new file mode 100644 index 0000000000..edd9cad152 --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/esc9aharness.json @@ -0,0 +1,314 @@ +{ + "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": 2055.0393057401334, + "y": 641.1074078540869 + }, + "caption": "Domain", + "labels": [], + "properties": { + "name": "d" + }, + "style": { + "node-color": "#68ccca" + } + }, + { + "id": "n2", + "position": { + "x": 1596.163360694819, + "y": 182.23146280877245 + }, + "caption": "NTAuthStore", + "labels": [], + "properties": {}, + "style": { + "node-color": "#653294", + "caption-color": "#ffffff" + } + }, + { + "id": "n3", + "position": { + "x": 1596.163360694819, + "y": 350.35614824592676 + }, + "caption": "RootCA", + "labels": [], + "properties": {}, + "style": { + "node-color": "#653294", + "caption-color": "#ffffff" + } + }, + { + "id": "n4", + "position": { + "x": 1092.7453131854052, + "y": 182.23146280877245 + }, + "caption": "EnterpriseCA", + "labels": [], + "properties": { + "name": "eca" + }, + "style": { + "node-color": "#194d33", + "caption-color": "#ffffff" + } + }, + { + "id": "n5", + "position": { + "x": 748.6949982406376, + "y": 182.23146280877245 + }, + "caption": "CertTemplate", + "labels": [], + "properties": { + "name": "ct", + "AuthenticationEnabled": "True", + "RequiresManagerApproval": "False", + "NoSecurityExtension": "True", + "SubjectAltNameRequireUPN": "True", + "SchemaVersion": "1", + "EnrolleeSuppliesSubject": "False", + "SubjectAltRequireSPN": "True" + }, + "style": { + "node-color": "#fda1ff" + } + }, + { + "id": "n6", + "position": { + "x": 748.6949982406376, + "y": 436.7832960176713 + }, + "caption": "AD principal (Victim)", + "labels": [], + "properties": { + "name": "vp" + }, + "style": { + "node-color": "#ffffff" + } + }, + { + "id": "n7", + "position": { + "x": 472.5320881490078, + "y": 641.1074078540869 + }, + "caption": "AD principal (attacker)", + "labels": [], + "properties": { + "name": "ap" + }, + "style": {} + }, + { + "id": "n8", + "position": { + "x": 129, + "y": 641.1074078540869 + }, + "caption": "", + "labels": [], + "properties": {}, + "style": { + "border-color": "#ffffff" + } + }, + { + "id": "n9", + "position": { + "x": 1596.163360694819, + "y": -4 + }, + "caption": "DC", + "labels": [], + "properties": { + "name": "dc" + }, + "style": { + "node-color": "#f44e3b" + } + } + ], + "relationships": [ + { + "id": "n0", + "fromId": "n5", + "toId": "n4", + "type": "PublishedTo", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n1", + "fromId": "n3", + "toId": "n1", + "type": "RootCAFor", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n2", + "fromId": "n6", + "toId": "n4", + "type": "Enroll", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n3", + "fromId": "n6", + "toId": "n5", + "type": "Enroll", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n4", + "fromId": "n4", + "toId": "n3", + "type": "IssuedSignedBy", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + }, + { + "id": "n5", + "fromId": "n2", + "toId": "n1", + "type": "NTAuthStoreFor", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n6", + "fromId": "n4", + "toId": "n2", + "type": "TrustedForNTAuth", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + }, + { + "id": "n7", + "fromId": "n7", + "toId": "n6", + "type": "GenericWrite", + "properties": {}, + "style": {} + }, + { + "id": "n8", + "fromId": "n7", + "toId": "n1", + "type": "ADCSESC9a", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + }, + { + "id": "n9", + "fromId": "n7", + "toId": "n8", + "type": "", + "properties": {}, + "style": { + "arrow-color": "#ffffff" + } + }, + { + "id": "n10", + "fromId": "n9", + "toId": "n1", + "type": "DCFor", + "properties": {}, + "style": { + "arrow-color": "#a4dd00" + } + }, + { + "id": "n11", + "fromId": "n4", + "toId": "n9", + "type": "CanAbuseWeakCertBinding", + "properties": {}, + "style": { + "arrow-color": "#7b64ff" + } + } + ] +} \ No newline at end of file diff --git a/cmd/api/src/test/integration/harnesses/esc9aharness.svg b/cmd/api/src/test/integration/harnesses/esc9aharness.svg new file mode 100644 index 0000000000..bd9dfa524b --- /dev/null +++ b/cmd/api/src/test/integration/harnesses/esc9aharness.svg @@ -0,0 +1,18 @@ + +PublishedToRootCAForEnrollEnrollIssuedSignedByNTAuthStoreForTrustedForNTAuthGenericWriteADCSESC9aDCForCanAbuseWeakCertBindingDomainname:dNTAuthStoreRootCAEnterpriseCAname:ecaCertTemplatename:ctAuthenticationEnabled:TrueRequiresManagerApproval:FalseNoSecurityExtension:TrueSubjectAltNameRequireUPN:TrueSchemaVersion:1EnrolleeSuppliesSubject:FalseSubjectAltRequireSPN:TrueADprincipal(Victim)name:vpADprincipal(attacker)name:apDCname:dc diff --git a/packages/go/analysis/ad/adcs.go b/packages/go/analysis/ad/adcs.go index 68115acbfe..eb64a1537e 100644 --- a/packages/go/analysis/ad/adcs.go +++ b/packages/go/analysis/ad/adcs.go @@ -479,6 +479,14 @@ func PostADCS(ctx context.Context, db graph.Database, groupExpansions impact.Pat } return nil }) + + operation.Operation.SubmitReader(func(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob) error { + if err := PostADCSESC9a(ctx, tx, outC, groupExpansions, innerEnterpriseCA, innerDomain, cache); err != nil { + log.Errorf("failed post processing for %s: %v", ad.ADCSESC9a.String(), err) + } + + return nil + }) } } diff --git a/packages/go/analysis/ad/esc9.go b/packages/go/analysis/ad/esc9.go new file mode 100644 index 0000000000..44aff9e63e --- /dev/null +++ b/packages/go/analysis/ad/esc9.go @@ -0,0 +1,175 @@ +// 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 + +package ad + +import ( + "context" + "errors" + "github.com/specterops/bloodhound/analysis" + "github.com/specterops/bloodhound/analysis/impact" + "github.com/specterops/bloodhound/dawgs/cardinality" + "github.com/specterops/bloodhound/dawgs/graph" + "github.com/specterops/bloodhound/dawgs/ops" + "github.com/specterops/bloodhound/dawgs/query" + "github.com/specterops/bloodhound/dawgs/util/channels" + "github.com/specterops/bloodhound/graphschema/ad" + "github.com/specterops/bloodhound/log" +) + +func PostADCSESC9a(ctx context.Context, tx graph.Transaction, outC chan<- analysis.CreatePostRelationshipJob, groupExpansions impact.PathAggregator, eca, domain *graph.Node, cache ADCSCache) error { + results := cardinality.NewBitmap32() + + if canAbuseWeakCertBindingRels, err := FetchCanAbuseWeakCertBindingRels(tx, eca); err != nil { + if graph.IsErrNotFound(err) { + return nil + } + + return err + } else if len(canAbuseWeakCertBindingRels) == 0 { + return nil + } else if publishedCertTemplates, ok := cache.PublishedTemplateCache[eca.ID]; !ok { + return nil + } else { + for _, template := range publishedCertTemplates { + if valid, err := isCertTemplateValidForESC9a(template); err != nil { + if !errors.Is(err, graph.ErrPropertyNotFound) { + log.Errorf("Error checking cert template validity for template %d: %v", template.ID, err) + } else { + log.Debugf("Error checking cert template validity for template %d: %v", template.ID, err) + } + } else if !valid { + continue + } else if certTemplateControllers, ok := cache.CertTemplateControllers[template.ID]; !ok { + log.Debugf("Failed to retrieve controllers for cert template %d from cache", template.ID) + continue + } else if ecaControllers, ok := cache.EnterpriseCAEnrollers[eca.ID]; !ok { + log.Debugf("Failed to retrieve controllers for enterprise ca %d from cache", eca.ID) + continue + } else { + //Expand controllers for the eca + template completely because we don't do group shortcutting here + var ( + victimBitmap = expandNodeSliceToBitmapWithoutGroups(certTemplateControllers, groupExpansions) + ecaBitmap = expandNodeSliceToBitmapWithoutGroups(ecaControllers, groupExpansions) + ) + + victimBitmap.And(ecaBitmap) + //Use our id list to filter down to users + if userNodes, err := ops.FetchNodeSet(tx.Nodes().Filterf(func() graph.Criteria { + return query.And( + query.KindIn(query.Node(), ad.User), + query.InIDs(query.NodeID(), cardinality.DuplexToGraphIDs(victimBitmap)...), + ) + })); err != nil { + if !graph.IsErrNotFound(err) { + return err + } + } else if len(userNodes) > 0 { + if subjRequireDns, err := template.Properties.Get(ad.SubjectAltRequireDNS.String()).Bool(); err != nil { + log.Debugf("Failed to retrieve subjectAltRequireDNS for template %d: %v", template.ID, err) + victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) + } else if subjRequireDomainDns, err := template.Properties.Get(ad.SubjectAltRequireDomainDNS.String()).Bool(); err != nil { + log.Debugf("Failed to retrieve subjectAltRequireDomainDNS for template %d: %v", template.ID, err) + victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) + } else if subjRequireDns || subjRequireDomainDns { + //If either of these properties is true, we need to remove all these users from our victims list + victimBitmap.Xor(cardinality.NodeSetToDuplex(userNodes)) + } + } + + if attackers, err := ops.FetchStartNodes(tx.Relationships().Filterf(func() graph.Criteria { + return query.And( + query.KindIn(query.Start(), ad.Group, ad.User, ad.Computer), + query.KindIn(query.Relationship(), ad.GenericAll, ad.GenericWrite, ad.Owns, ad.WriteOwner, ad.WriteDACL), + query.InIDs(query.EndID(), cardinality.DuplexToGraphIDs(victimBitmap)...), + ) + })); err != nil { + return err + } else { + results.Or(cardinality.NodeSetToDuplex(attackers)) + } + } + } + + results.Each(func(value uint32) (bool, error) { + if !channels.Submit(ctx, outC, analysis.CreatePostRelationshipJob{ + FromID: graph.ID(value), + ToID: domain.ID, + Kind: ad.ADCSESC9a, + }) { + return false, nil + } else { + return true, nil + } + }) + + return nil + } +} + +func expandNodeSliceToBitmapWithoutGroups(nodes []*graph.Node, groupExpansions impact.PathAggregator) cardinality.Duplex[uint32] { + var bitmap = cardinality.NewBitmap32() + for _, controller := range nodes { + if controller.Kinds.ContainsOneOf(ad.Group) { + groupExpansions.Cardinality(controller.ID.Uint32()).(cardinality.Duplex[uint32]).Each(func(id uint32) (bool, error) { + //Check group expansions against each id, if cardinality is 0 than its not a group + if groupExpansions.Cardinality(id).Cardinality() == 0 { + bitmap.Add(id) + } + + return true, nil + }) + } else { + bitmap.Add(controller.ID.Uint32()) + } + } + + return bitmap +} + +func isCertTemplateValidForESC9a(ct *graph.Node) (bool, error) { + if reqManagerApproval, err := ct.Properties.Get(ad.RequiresManagerApproval.String()).Bool(); err != nil { + return false, err + } else if reqManagerApproval { + return false, nil + } else if authenticationEnabled, err := ct.Properties.Get(ad.AuthenticationEnabled.String()).Bool(); err != nil { + return false, err + } else if !authenticationEnabled { + return false, nil + } else if noSecurityExtension, err := ct.Properties.Get(ad.NoSecurityExtension.String()).Bool(); err != nil { + return false, err + } else if !noSecurityExtension { + return false, nil + } else if enrolleeSuppliesSubject, err := ct.Properties.Get(ad.EnrolleeSuppliesSubject.String()).Bool(); err != nil { + return false, err + } else if enrolleeSuppliesSubject { + return false, nil + } else if schemaVersion, err := ct.Properties.Get(ad.SchemaVersion.String()).Float64(); err != nil { + return false, err + } else if authorizedSignatures, err := ct.Properties.Get(ad.AuthorizedSignatures.String()).Float64(); err != nil { + return false, err + } else if schemaVersion > 1 && authorizedSignatures > 0 { + return false, nil + } else if subjectAltRequireUPN, err := ct.Properties.Get(ad.SubjectAltRequireUPN.String()).Bool(); err != nil { + return false, err + } else if subjectAltRequireSPN, err := ct.Properties.Get(ad.SubjectAltRequireSPN.String()).Bool(); err != nil { + return false, err + } else if subjectAltRequireSPN || subjectAltRequireUPN { + return true, nil + } else { + return false, nil + } +} diff --git a/packages/go/graphschema/azure/azure.go b/packages/go/graphschema/azure/azure.go index 2d551de659..8262cbf3a0 100644 --- a/packages/go/graphschema/azure/azure.go +++ b/packages/go/graphschema/azure/azure.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/common/common.go b/packages/go/graphschema/common/common.go index 058969f506..6fd161585e 100644 --- a/packages/go/graphschema/common/common.go +++ b/packages/go/graphschema/common/common.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// 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. diff --git a/packages/go/graphschema/graph.go b/packages/go/graphschema/graph.go index 34a7f9b11b..3deedefca6 100644 --- a/packages/go/graphschema/graph.go +++ b/packages/go/graphschema/graph.go @@ -1,4 +1,4 @@ -// Copyright 2023 Specter Ops, Inc. +// 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. From cbeaef0079874aa5398d4a4ebb206ffcd1ac4c37 Mon Sep 17 00:00:00 2001 From: Ben Waples Date: Fri, 26 Jan 2024 13:53:39 -0800 Subject: [PATCH 2/3] ESC 9b Finding Panel (#349) * initial setup * some qa * remove hardcoded testing and smol qa * shared help text styles --- .../HelpTexts/ADCSESC10a/General.tsx | 21 +-- .../HelpTexts/ADCSESC10a/LinuxAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC10a/WindowsAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC9a/General.tsx | 21 +-- .../HelpTexts/ADCSESC9a/LinuxAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC9a/WindowsAbuse.tsx | 20 +-- .../HelpTexts/ADCSESC9b/ADCSESC9b.tsx | 31 ++++ .../HelpTexts/ADCSESC9b/General.tsx | 59 +++++++ .../HelpTexts/ADCSESC9b/LinuxAbuse.tsx | 156 +++++++++++++++++ .../components/HelpTexts/ADCSESC9b/Opsec.tsx | 31 ++++ .../HelpTexts/ADCSESC9b/References.tsx | 75 ++++++++ .../HelpTexts/ADCSESC9b/WindowsAbuse.tsx | 164 ++++++++++++++++++ .../src/components/HelpTexts/index.tsx | 2 + .../src/components/HelpTexts/utils.ts | 19 ++ 14 files changed, 549 insertions(+), 110 deletions(-) create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx index b00ca9b1b4..02c61e3567 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/General.tsx @@ -15,29 +15,12 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import { groupSpecialFormat } from '../utils'; +import { useHelpTextStyles, groupSpecialFormat } from '../utils'; import { EdgeInfoProps } from '../index'; import { Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); const General: FC = ({ sourceName, sourceType, targetName }) => { - const classes = useStyles(); + const classes = useHelpTextStyles(); return ( {groupSpecialFormat(sourceType, sourceName)} has the privileges to perform the ADCS ESC10 Scenario A attack diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx index e7987bc68f..b86b3aa9d4 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/LinuxAbuse.tsx @@ -16,26 +16,10 @@ import { FC } from 'react'; import { Box, Link, List, ListItem, Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const LinuxAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx index fc0652d1ef..dd99094430 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC10a/WindowsAbuse.tsx @@ -15,27 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import makeStyles from '@mui/styles/makeStyles'; import { Typography, Link, List, ListItem, Box } from '@mui/material'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const WindowsAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx index a53caf54b2..6d1b25bb99 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/General.tsx @@ -15,29 +15,12 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import { groupSpecialFormat } from '../utils'; +import { useHelpTextStyles, groupSpecialFormat } from '../utils'; import { EdgeInfoProps } from '../index'; import { Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); const General: FC = ({ sourceName, sourceType, targetName }) => { - const classes = useStyles(); + const classes = useHelpTextStyles(); return ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx index dbea805e69..f990bed666 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/LinuxAbuse.tsx @@ -16,26 +16,10 @@ import { FC } from 'react'; import { Box, Link, List, ListItem, Typography } from '@mui/material'; -import { makeStyles } from '@mui/styles'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const LinuxAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx index 43bfac3928..844173b1f4 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9a/WindowsAbuse.tsx @@ -15,27 +15,11 @@ // SPDX-License-Identifier: Apache-2.0 import { FC } from 'react'; -import makeStyles from '@mui/styles/makeStyles'; import { Typography, Link, List, ListItem, Box } from '@mui/material'; - -const useStyles = makeStyles((theme) => ({ - containsCodeEl: { - '& code': { - backgroundColor: 'darkgrey', - padding: '2px .5ch', - fontWeight: 'normal', - fontSize: '.875em', - borderRadius: '3px', - display: 'inline', - - overflowWrap: 'break-word', - whiteSpace: 'pre-wrap', - }, - }, -})); +import { useHelpTextStyles } from '../utils'; const WindowsAbuse: FC = () => { - const classes = useStyles(); + const classes = useHelpTextStyles(); const step1 = ( <> diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx new file mode 100644 index 0000000000..724cfe762e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/ADCSESC9b.tsx @@ -0,0 +1,31 @@ +// 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 General from './General'; +import WindowsAbuse from './WindowsAbuse'; +import LinuxAbuse from './LinuxAbuse'; +import Opsec from './Opsec'; +import References from './References'; + +const ADCSESC9b = { + general: General, + windowsAbuse: WindowsAbuse, + linuxAbuse: LinuxAbuse, + opsec: Opsec, + references: References, +}; + +export default ADCSESC9b; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx new file mode 100644 index 0000000000..6ca89a6682 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/General.tsx @@ -0,0 +1,59 @@ +// 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 { useHelpTextStyles, groupSpecialFormat } from '../utils'; +import { EdgeInfoProps } from '../index'; +import { Typography } from '@mui/material'; + +const General: FC = ({ sourceName, sourceType, targetName }) => { + const classes = useHelpTextStyles(); + return ( + <> + + {groupSpecialFormat(sourceType, sourceName)} has the privileges to perform the ADCS ESC9 Scenario B + attack against the target domain. +
+
+ The principal has control over a victim computer with permission to enroll on one or more certificate + templates, configured to: 1) enable certificate authentication, 2) require the dNSHostName +  of the enrollee included in the Subject Alternative Name (SAN), and 3) not have the security + extension enabled. The victim computer also has enrollment permission for an enterprise CA with the + necessary templates published. This enterprise CA is trusted for NT authentication in the forest, and + chains up to a root CA for the forest. There is an affected Domain Controller (DC) configured to allow + weak certificate binding enforcement. This setup lets the principal impersonate any AD forest computer + without their credentials. +
+
+ The attacker principal can abuse their control over the victim computer to modify the victim computer's{' '} + dNSHostName attribute to match the dNSHostName of a targeted computer. The + attacker principal will then abuse their control over the victim computer to obtain the credentials of + the victim computer, or a session as the victim computer, and enroll a certificate as the victim in one + of the affected certificate templates. The dNSHostName of the victim will be included in + the issued certificate under SAN DNS name. As the certificate template does not have the security + extension, the issued certificate will NOT include the SID of the victim computer. DCs with strong + certificate binding configuration will require a SID to be present in a certificate used for Kerberos + authentication, but the affected DCs with weak certificate binding configuration will not. The affected + DCs will split the SAN DNS name into a computer name and a domain name, confirm that the domain name is + correct, and use the computer name appended a $ to identify principals with a matching{' '} + sAMAccountName. At last, the DC issues a Kerberos TGT as the targeted computer to the + attacker, which means the attacker now has a session as the targeted computer. +
+ + ); +}; + +export default General; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx new file mode 100644 index 0000000000..fa5a0e84ee --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/LinuxAbuse.tsx @@ -0,0 +1,156 @@ +// 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 { Link, Typography } from '@mui/material'; +import { useHelpTextStyles } from '../utils'; + +const LinuxAbuse: FC = () => { + const classes = useHelpTextStyles(); + const step1 = ( + <> + + Step 1: Set dNSHostName of victim computer to targeted computer's{' '} + dNSHostName. +
+
+ Set the dNSHostName of the victim computer using Certipy: +
+ + { + 'certipy account update -username ATTACKER@CORP.LOCAL -password PWD -user VICTIM -dns TARGET.CORP.LOCAL' + } + + + ); + + const step2 = ( + <> + + Step 2: Check if mail attribute of victim must be set and set it if required. +
+
+ If the certificate template is of schema version 2 or above and its attribute{' '} + msPKI-CertificateNameFlag contains the flag SUBJECT_REQUIRE_EMAIL and/or + SUBJECT_ALT_REQUIRE_EMAIL then the victim principal must have their mail attribute set for + the certificate enrollment. The CertTemplate BloodHound node will have "Subject Require Email"{' '} + or "Subject Alternative Name Require Email" set to true if any of the flags are present. +
+
+ If the certificate template is of schema version 1 or does not have any of the email flags, then + continue to Step 3. +
+
+ If any of the two flags are present, you will need the victim's mail attribute to be set. The value of + the attribute will be included in the issues certificate but it is not used to identify the target + computer why it can be set to any arbitrary string. +
+
+ Check if the victim has the mail attribute set using ldapsearch: +
+ {`ldapsearch -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME -b "VICTIM-DN" mail`} + + If the victim has the mail attribute set, continue to Step 3. +
+
+ If the victim does not have the mail attribute set, set it to a dummy mail using ldapmodify: +
+ + {`echo -e "dn: VICTIM-DN\nchangetype: modify\nreplace: mail\nmail: test@mail.com" | ldapmodify -x -D "ATTACKER-DN" -w 'PWD' -h DOMAIN-DNS-NAME`} + + + ); + + const step3 = ( + + Step 3: Obtain a session as victim. +
+
+ There are several options for this step. You can obtain a session as SYSTEM on the host, which allows you to + interact with AD as the computer account, by abusing control over the computer AD object (see{' '} + + GenericAll edge documentation + + ) +
+ ); + + const step4 = ( + <> + + Step 4: Enroll certificate as victim. +
+
+ Use Certipy as the victim computer to request enrollment in the affected template, specifying the + affected EnterpriseCA: +
+ + {'certipy req -u VICTIM@CORP.LOCAL -p PWD -ca CA-NAME -target SERVER -template TEMPLATE'} + + + The issued certificate will be saved to disk with the name of the targeted computer. + + + ); + + const step5 = ( + <> + + Step 5 (Optional): Set dNSHostName of victim to the previous value. +
+
+ To avoid DNS issues in the environment, set the dNSHostName of the victim computer back to + its previous value using Certipy: +
+ + { + 'certipy account update -username ATTACKER@CORP.LOCAL -password PWD -user VICTIM -dns VICTIM.CORP.LOCAL' + } + + + ); + + const step6 = ( + <> + + Step 6: Perform Kerberos authentication as targeted computer against affected DC using + certificate. +
+
+ Request a ticket granting ticket (TGT) from the domain, specifying the certificate created in Step 4 and + the IP of an affected DC: +
+ {'certipy auth -pfx TARGET.pfx -dc-ip IP'} + + ); + + return ( + <> + An attacker may perform this attack in the following steps: + {step1} + {step2} + {step3} + {step4} + {step5} + {step6} + + ); +}; + +export default LinuxAbuse; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx new file mode 100644 index 0000000000..cc73a72e2e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/Opsec.tsx @@ -0,0 +1,31 @@ +// 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 { Typography } from '@mui/material'; + +const Opsec: FC = () => { + return ( + + When the affected certificate authority issues the certificate to the attacker, it will retain a local copy + of that certificate in its issued certificates store. Defenders may analyze those issued certificates to + identify illegitimately issued certificates and identify the computer that requested the certificate, as + well as the target identity the attacker is attempting to impersonate. + + ); +}; + +export default Opsec; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx new file mode 100644 index 0000000000..1ac6d3fe1c --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/References.tsx @@ -0,0 +1,75 @@ +// 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 React, { FC } from 'react'; +import { Link, Box } from '@mui/material'; + +const References: FC = () => { + const references = [ + { + label: 'Certipy 4.0', + link: 'https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7', + }, + { + label: 'Certified Pre-Owned', + link: 'https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf', + }, + { + label: 'Certipy', + link: 'https://github.com/ly4k/Certipy', + }, + { + label: 'GhostPack Certipy', + link: 'https://github.com/GhostPack/Certify', + }, + { + label: 'GhostPack Rubeus', + link: 'https://github.com/GhostPack/Rubeus', + }, + { + label: 'Set-DomainObject', + link: 'https://powersploit.readthedocs.io/en/latest/Recon/Set-DomainObject', + }, + { + label: 'CertUtil.exe', + link: 'https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/certutil', + }, + { + label: 'LDAPSearch', + link: 'https://linux.die.net/man/1/ldapsearch', + }, + { + label: 'LDAPModify', + link: 'https://linux.die.net/man/1/ldapmodify', + }, + ]; + return ( + + {references.map((reference) => { + return ( + + + {reference.label} + +
+
+ ); + })} +
+ ); +}; + +export default References; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx new file mode 100644 index 0000000000..35305c6e9e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/ADCSESC9b/WindowsAbuse.tsx @@ -0,0 +1,164 @@ +// 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 { Typography, Link } from '@mui/material'; +import { useHelpTextStyles } from '../utils'; + +const WindowsAbuse: FC = () => { + const classes = useHelpTextStyles(); + const step1 = ( + <> + + Step 1: Set dNSHostName of victim computer to targeted computer's{' '} + dNSHostName. +
+
+ Set the dNSHostName of the victim computer using PowerView: +
+ + {"Set-DomainObject -Identity VICTIM -Set @{'dnshostname'='target.corp.local'}"} + + + ); + + const step2 = ( + <> + + Step 2: Check if mail attribute of victim must be set and set it if required. +
+
+ If the certificate template is of schema version 2 or above and its attribute{' '} + msPKI-CertificateNameFlag contains the flag SUBJECT_REQUIRE_EMAIL and/or{' '} + SUBJECT_ALT_REQUIRE_EMAIL then the victim principal must have their mail{' '} + attribute set for the certificate enrollment. The CertTemplate BloodHound node will have{' '} + "Subject Require Email" or "Subject Alternative Name Require Email" set to true if any + of the flags are present. + "Subject Alternative Name Require Email" set to true if any of the flags are present. +
+
+ If the certificate template is of schema version 1 or does not have any of the email flags, then + continue to Step 3. +
+
+ If any of the two flags are present, you will need the victim's mail attribute to be set. The value of + the attribute will be included in the issues certificate but it is not used to identify the target + computer why it can be set to any arbitrary string. +
+
+ Check if the victim has the mail attribute set using PowerView: +
+ {'Get-DomainObject -Identity VICTIM -Properties mail'} + + If the victim has the mail attribute set, continue to Step 3. +
+
+ If the victim does not have the mail attribute set, set it to a dummy mail using PowerView: +
+ + {"Set-DomainObject -Identity VICTIM -Set @{'mail'='dummy@mail.com'}"} + + + ); + + const step3 = ( + + Step 3: Obtain a session as victim. +
+
+ There are several options for this step. You can obtain a session as SYSTEM on the host, which allows you to + interact with AD as the computer account, by abusing control over the computer AD object (see{' '} + + GenericAll edge documentation + + ). +
+ ); + + const step4 = ( + <> + + Step 4: Enroll certificate as victim. +
+
+ Use Certify as the victim computer to request enrollment in the affected template, specifying the + affected EnterpriseCA: +
+ + {'Certify.exe request /ca:SERVERCA-NAME /template:TEMPLATE /machine'} + + + Save the certificate as cert.pem and the private key as cert.key. + + + ); + + const step5 = ( + <> + + Step 5: Convert the emitted certificate to PFX format: + + {'certutil.exe -MergePFX .cert.pem .cert.pfx'} + + ); + const step6 = ( + <> + + Step 6 (Optional): Set dNSHostName of victim to the previous value. +
+
+ To avoid DNS issues in the environment, set the dNSHostName of the victim computer back to + its previous value using PowerView: +
+ + {"Set-DomainObject -Identity VICTIM -Set @{'dnshostname'='victim.corp.local'}"} + + + ); + const step7 = ( + <> + + Step 7: Perform Kerberos authentication as targeted computer against affected DC using + certificate. +
+
+ Use Rubeus to request a ticket granting ticket (TGT) from an affected DC, specifying the target identity + to impersonate and the PFX-formatted certificate created in Step 5: +
+ + {'Rubeus.exe asktgt /certificate:cert.pfx /user:TARGET$ /domain:DOMAIN /dc:DOMAIN_CONTROLLER'} + + + ); + + return ( + <> + An attacker may perform this attack in the following steps. + {step1} + {step2} + {step3} + {step4} + {step5} + {step6} + {step7} + + ); +}; + +export default WindowsAbuse; diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx b/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx index d3927d2878..03d4971509 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/index.tsx @@ -112,6 +112,7 @@ import ADCSESC1 from './ADCSESC1/ADCSESC1'; import ADCSESC6a from './ADCSESC6a/ADCSESC6a'; import ADCSESC6b from './ADCSESC6b/ADCSESC6b'; import ADCSESC9a from './ADCSESC9a/ADCSESC9a'; +import ADCSESC9b from './ADCSESC9b/ADCSESC9b'; import ADCSESC10a from './ADCSESC10a/ADCSESC10a'; export type EdgeInfoProps = { @@ -218,6 +219,7 @@ const EdgeInfoComponents = { ADCSESC6a: ADCSESC6a, ADCSESC6b: ADCSESC6b, ADCSESC9a: ADCSESC9a, + ADCSESC9b: ADCSESC9b, ADCSESC10a: ADCSESC10a, ManageCA: ManageCA, ManageCertificates: ManageCertificates, diff --git a/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts b/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts index 0ad6a85cb4..7a6caf3abe 100644 --- a/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts +++ b/packages/javascript/bh-shared-ui/src/components/HelpTexts/utils.ts @@ -14,6 +14,8 @@ // // SPDX-License-Identifier: Apache-2.0 +import { makeStyles } from "@mui/styles"; + export const groupSpecialFormat = (sourceType: string | undefined, sourceName: string | undefined) => { if (!sourceType || !sourceName) return 'This entity has'; if (sourceType === 'Group') { @@ -41,3 +43,20 @@ export const typeFormat = (type: string | undefined): string => { return type.toLowerCase(); } }; + + +export const useHelpTextStyles = makeStyles((theme) => ({ + containsCodeEl: { + '& code': { + backgroundColor: 'darkgrey', + padding: '2px .5ch', + fontWeight: 'normal', + fontSize: '.875em', + borderRadius: '3px', + display: 'inline', + + overflowWrap: 'break-word', + whiteSpace: 'pre-wrap', + }, + }, +})); \ No newline at end of file From a2321982bf151502997c5d43d148854549cfd542 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Mon, 29 Jan 2024 08:55:54 -0800 Subject: [PATCH 3/3] chore: remove unnecessary complexity in the data pipeline (#347) * chore: remove unnecessary complexity in the data pipeline * fix: remove all in-memory state management from data pipeline --- cmd/api/src/api/v2/file_uploads.go | 3 +- cmd/api/src/api/v2/file_uploads_test.go | 9 +- cmd/api/src/daemons/datapipe/datapipe.go | 90 ++++++++------ cmd/api/src/daemons/datapipe/jobs.go | 117 +++++++++--------- cmd/api/src/daemons/datapipe/mocks/mock.go | 12 -- cmd/api/src/model/jobs.go | 5 + .../src/services/fileupload/file_upload.go | 27 ++-- .../src/components/FinishedIngestLog/types.ts | 3 + 8 files changed, 133 insertions(+), 133 deletions(-) diff --git a/cmd/api/src/api/v2/file_uploads.go b/cmd/api/src/api/v2/file_uploads.go index 0f73c1a94a..8a9512fa2e 100644 --- a/cmd/api/src/api/v2/file_uploads.go +++ b/cmd/api/src/api/v2/file_uploads.go @@ -153,10 +153,9 @@ func (s Resources) EndFileUploadJob(response http.ResponseWriter, request *http. api.HandleDatabaseError(request, response, err) } else if fileUploadJob.Status != model.JobStatusRunning { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "job must be in running status to end", request), response) - } else if fileUploadJob, err := fileupload.EndFileUploadJob(s.DB, fileUploadJob); err != nil { + } else if err := fileupload.EndFileUploadJob(s.DB, fileUploadJob); err != nil { api.HandleDatabaseError(request, response, err) } else { - s.TaskNotifier.NotifyOfFileUploadJobStatus(fileUploadJob) response.WriteHeader(http.StatusOK) } } diff --git a/cmd/api/src/api/v2/file_uploads_test.go b/cmd/api/src/api/v2/file_uploads_test.go index 571b6bce0a..f48aca60ed 100644 --- a/cmd/api/src/api/v2/file_uploads_test.go +++ b/cmd/api/src/api/v2/file_uploads_test.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 v2_test @@ -216,7 +216,6 @@ func TestResources_EndFileUploadJob(t *testing.T) { Status: model.JobStatusRunning, }, nil) mockDB.EXPECT().UpdateFileUploadJob(gomock.Any()).Return(nil) - mockTasker.EXPECT().NotifyOfFileUploadJobStatus(gomock.Any()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusOK) diff --git a/cmd/api/src/daemons/datapipe/datapipe.go b/cmd/api/src/daemons/datapipe/datapipe.go index 2baf804def..a47ee54a39 100644 --- a/cmd/api/src/daemons/datapipe/datapipe.go +++ b/cmd/api/src/daemons/datapipe/datapipe.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "sync" + "sync/atomic" "time" "github.com/specterops/bloodhound/cache" @@ -40,24 +41,19 @@ const ( ) type Tasker interface { - NotifyOfFileUploadJobStatus(task model.FileUploadJob) RequestAnalysis() GetStatus() model.DatapipeStatusWrapper } type Daemon struct { - db database.Database - graphdb graph.Database - cache cache.Cache - cfg config.Configuration - analysisRequested bool - tickInterval time.Duration - status model.DatapipeStatusWrapper - ctx context.Context - fileUploadJobIDsUnderAnalysis []int64 - completedFileUploadJobIDs []int64 - - lock *sync.Mutex + db database.Database + graphdb graph.Database + cache cache.Cache + cfg config.Configuration + analysisRequested *atomic.Bool + tickInterval time.Duration + status model.DatapipeStatusWrapper + ctx context.Context clearOrphanedFilesLock *sync.Mutex } @@ -67,14 +63,12 @@ func (s *Daemon) Name() string { func NewDaemon(ctx context.Context, cfg config.Configuration, connections bootstrap.DatabaseConnections[*database.BloodhoundDB, *graph.DatabaseSwitch], cache cache.Cache, tickInterval time.Duration) *Daemon { return &Daemon{ - db: connections.RDMS, - graphdb: connections.Graph, - cache: cache, - cfg: cfg, - ctx: ctx, - - analysisRequested: false, - lock: &sync.Mutex{}, + db: connections.RDMS, + graphdb: connections.Graph, + cache: cache, + cfg: cfg, + ctx: ctx, + analysisRequested: &atomic.Bool{}, clearOrphanedFilesLock: &sync.Mutex{}, tickInterval: tickInterval, status: model.DatapipeStatusWrapper{ @@ -93,42 +87,41 @@ func (s *Daemon) GetStatus() model.DatapipeStatusWrapper { } func (s *Daemon) getAnalysisRequested() bool { - s.lock.Lock() - defer s.lock.Unlock() - return s.analysisRequested + return s.analysisRequested.Load() } func (s *Daemon) setAnalysisRequested(requested bool) { - s.lock.Lock() - defer s.lock.Unlock() - s.analysisRequested = requested + s.analysisRequested.Store(requested) } func (s *Daemon) analyze() { + // Ensure that the user-requested analysis switch is flipped back to false. This is done at the beginning of the + // function so that any re-analysis requests are caught while analysis is in-progress. + s.setAnalysisRequested(false) + if s.cfg.DisableAnalysis { return } s.status.Update(model.DatapipeStatusAnalyzing, false) - log.Measure(log.LevelInfo, "Starting analysis")() + log.LogAndMeasure(log.LevelInfo, "Graph Analysis")() if err := RunAnalysisOperations(s.ctx, s.db, s.graphdb, s.cfg); err != nil { - log.Errorf("Analysis failed: %v", err) + log.Errorf("Graph analysis failed: %v", err) s.failJobsUnderAnalysis() s.status.Update(model.DatapipeStatusIdle, false) } else { + s.completeJobsUnderAnalysis() + if entityPanelCachingFlag, err := s.db.GetFlagByKey(appcfg.FeatureEntityPanelCaching); err != nil { log.Errorf("Error retrieving entity panel caching flag: %v", err) } else { resetCache(s.cache, entityPanelCachingFlag.Enabled) } - s.clearJobsFromAnalysis() - log.Measure(log.LevelInfo, "Analysis run finished")() + s.status.Update(model.DatapipeStatusIdle, true) } - - s.setAnalysisRequested(false) } func resetCache(cacher cache.Cache, cacheEnabled bool) { @@ -143,10 +136,22 @@ func (s *Daemon) ingestAvailableTasks() { if ingestTasks, err := s.db.GetAllIngestTasks(); err != nil { log.Errorf("Failed fetching available ingest tasks: %v", err) } else { - s.processIngestTasks(ingestTasks) + s.processIngestTasks(s.ctx, ingestTasks) } } +func (s *Daemon) getNumJobsWaitingForAnalysis() (int, error) { + numJobsWaitingForAnalysis := 0 + + if fileUploadJobsUnderAnalysis, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusAnalyzing); err != nil { + return 0, err + } else { + numJobsWaitingForAnalysis += len(fileUploadJobsUnderAnalysis) + } + + return numJobsWaitingForAnalysis, nil +} + func (s *Daemon) Start() { var ( datapipeLoopTimer = time.NewTimer(s.tickInterval) @@ -164,15 +169,20 @@ func (s *Daemon) Start() { s.clearOrphanedData() case <-datapipeLoopTimer.C: + // Ingest all available ingest tasks + s.ingestAvailableTasks() + + // Manage time-out state progression for file upload jobs fileupload.ProcessStaleFileUploadJobs(s.db) - if s.numAvailableCompletedFileUploadJobs() > 0 { - s.processCompletedFileUploadJobs() - s.analyze() - } else if s.getAnalysisRequested() { + // Manage nominal state transitions for file upload jobs + s.processIngestedFileUploadJobs() + + // If there are completed file upload jobs or if analysis was user-requested, perform analysis. + if numJobsWaitingForAnalysis, err := s.getNumJobsWaitingForAnalysis(); err != nil { + log.Errorf("Failed looking up jobs waiting for analysis: %v", err) + } else if numJobsWaitingForAnalysis > 0 || s.getAnalysisRequested() { s.analyze() - } else { - s.ingestAvailableTasks() } datapipeLoopTimer.Reset(s.tickInterval) diff --git a/cmd/api/src/daemons/datapipe/jobs.go b/cmd/api/src/daemons/datapipe/jobs.go index 908edd3a41..b80a028c98 100644 --- a/cmd/api/src/daemons/datapipe/jobs.go +++ b/cmd/api/src/daemons/datapipe/jobs.go @@ -17,6 +17,7 @@ package datapipe import ( + "context" "os" "github.com/specterops/bloodhound/dawgs/graph" @@ -25,78 +26,88 @@ import ( "github.com/specterops/bloodhound/src/services/fileupload" ) -func (s *Daemon) numAvailableCompletedFileUploadJobs() int { - s.lock.Lock() - defer s.lock.Unlock() - - return len(s.completedFileUploadJobIDs) -} - func (s *Daemon) failJobsUnderAnalysis() { - for _, jobID := range s.fileUploadJobIDsUnderAnalysis { - if err := fileupload.FailFileUploadJob(s.db, jobID, "Analysis failed"); err != nil { - log.Errorf("Failed updating job %d to failed status: %v", jobID, err) + if fileUploadJobsUnderAnalysis, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusAnalyzing); err != nil { + log.Errorf("Failed to load file upload jobs under analysis: %v", err) + } else { + for _, job := range fileUploadJobsUnderAnalysis { + if err := fileupload.FailFileUploadJob(s.db, job.ID, "Analysis failed"); err != nil { + log.Errorf("Failed updating file upload job %d to failed status: %v", job.ID, err) + } } } - - s.clearJobsFromAnalysis() -} - -func (s *Daemon) clearJobsFromAnalysis() { - s.lock.Lock() - s.fileUploadJobIDsUnderAnalysis = s.fileUploadJobIDsUnderAnalysis[:0] - s.lock.Unlock() } -func (s *Daemon) processCompletedFileUploadJobs() { - completedJobIDs := s.getAndTransitionCompletedJobIDs() - - for _, id := range completedJobIDs { - if ingestTasks, err := s.db.GetIngestTasksForJob(id); err != nil { - log.Errorf("Failed fetching available ingest tasks: %v", err) - } else { - s.processIngestTasks(ingestTasks) +func (s *Daemon) completeJobsUnderAnalysis() { + if fileUploadJobsUnderAnalysis, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusAnalyzing); err != nil { + log.Errorf("Failed to load file upload jobs under analysis: %v", err) + } else { + for _, job := range fileUploadJobsUnderAnalysis { + if err := fileupload.UpdateFileUploadJobStatus(s.db, job, model.JobStatusComplete, "Complete"); err != nil { + log.Errorf("Error updating fileupload job %d: %v", job.ID, err) + } } + } +} - if err := fileupload.UpdateFileUploadJobStatus(s.db, id, model.JobStatusComplete, "Complete"); err != nil { - log.Errorf("Error updating fileupload job %d: %v", id, err) +func (s *Daemon) processIngestedFileUploadJobs() { + if ingestedFileUploadJobs, err := s.db.GetFileUploadJobsWithStatus(model.JobStatusIngesting); err != nil { + log.Errorf("Failed to look up finished file upload jobs: %v", err) + } else { + for _, ingestedFileUploadJob := range ingestedFileUploadJobs { + if err := fileupload.UpdateFileUploadJobStatus(s.db, ingestedFileUploadJob, model.JobStatusAnalyzing, "Analyzing"); err != nil { + log.Errorf("Error updating fileupload job %d: %v", ingestedFileUploadJob.ID, err) + } } } } -func (s *Daemon) getAndTransitionCompletedJobIDs() []int64 { - s.lock.Lock() - defer s.lock.Unlock() +// clearFileTask removes a generic file upload task for ingested data. +func (s *Daemon) clearFileTask(ingestTask model.IngestTask) { + if err := s.db.DeleteIngestTask(ingestTask); err != nil { + log.Errorf("Error removing file upload task from db: %v", err) + } +} - // transition completed jobs to analysis - s.fileUploadJobIDsUnderAnalysis = append(s.fileUploadJobIDsUnderAnalysis, s.completedFileUploadJobIDs...) - s.completedFileUploadJobIDs = s.completedFileUploadJobIDs[:0] +func (s *Daemon) processIngestFile(ctx context.Context, path string) error { + if jsonFile, err := os.Open(path); err != nil { + return err + } else { + defer func() { + if err := jsonFile.Close(); err != nil { + log.Errorf("Failed closing ingest file %s: %v", path, err) + } + }() - return s.fileUploadJobIDsUnderAnalysis + return s.graphdb.BatchOperation(ctx, func(batch graph.Batch) error { + if err := s.ReadWrapper(batch, jsonFile); err != nil { + return err + } else { + return nil + } + }) + } } -func (s *Daemon) processIngestTasks(ingestTasks model.IngestTasks) { +// processIngestTasks covers the generic file upload case for ingested data. +func (s *Daemon) processIngestTasks(ctx context.Context, ingestTasks model.IngestTasks) { s.status.Update(model.DatapipeStatusIngesting, false) defer s.status.Update(model.DatapipeStatusIdle, false) for _, ingestTask := range ingestTasks { - jsonFile, err := os.Open(ingestTask.FileName) - if err != nil { - log.Errorf("Error reading file for ingest task %v: %v", ingestTask.ID, err) + // Check the context to see if we should continue processing ingest tasks. This has to be explicit since error + // handling assumes that all failures should be logged and not returned. + select { + case <-ctx.Done(): + return + default: } - if err = s.graphdb.BatchOperation(s.ctx, func(batch graph.Batch) error { - if err := s.ReadWrapper(batch, jsonFile); err != nil { - return err - } else { - return nil - } - }); err != nil { - log.Errorf("Error processing ingest task %v: %v", ingestTask.ID, err) + if err := s.processIngestFile(ctx, ingestTask.FileName); err != nil { + log.Errorf("Failed processing ingest task %d with file %s: %v", ingestTask.ID, ingestTask.FileName, err) } - s.clearTask(ingestTask) - jsonFile.Close() + s.clearFileTask(ingestTask) } } @@ -105,11 +116,3 @@ func (s *Daemon) clearTask(ingestTask model.IngestTask) { log.Errorf("Error removing task from db: %v", err) } } - -func (s *Daemon) NotifyOfFileUploadJobStatus(job model.FileUploadJob) { - if job.Status == model.JobStatusIngesting { - s.lock.Lock() - s.completedFileUploadJobIDs = append(s.completedFileUploadJobIDs, job.ID) - s.lock.Unlock() - } -} diff --git a/cmd/api/src/daemons/datapipe/mocks/mock.go b/cmd/api/src/daemons/datapipe/mocks/mock.go index 8148722ec5..2c9a4d61a9 100644 --- a/cmd/api/src/daemons/datapipe/mocks/mock.go +++ b/cmd/api/src/daemons/datapipe/mocks/mock.go @@ -64,18 +64,6 @@ func (mr *MockTaskerMockRecorder) GetStatus() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatus", reflect.TypeOf((*MockTasker)(nil).GetStatus)) } -// NotifyOfFileUploadJobStatus mocks base method. -func (m *MockTasker) NotifyOfFileUploadJobStatus(arg0 model.FileUploadJob) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "NotifyOfFileUploadJobStatus", arg0) -} - -// NotifyOfFileUploadJobStatus indicates an expected call of NotifyOfFileUploadJobStatus. -func (mr *MockTaskerMockRecorder) NotifyOfFileUploadJobStatus(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotifyOfFileUploadJobStatus", reflect.TypeOf((*MockTasker)(nil).NotifyOfFileUploadJobStatus), arg0) -} - // RequestAnalysis mocks base method. func (m *MockTasker) RequestAnalysis() { m.ctrl.T.Helper() diff --git a/cmd/api/src/model/jobs.go b/cmd/api/src/model/jobs.go index bb305f37ff..4a0ca0841e 100644 --- a/cmd/api/src/model/jobs.go +++ b/cmd/api/src/model/jobs.go @@ -116,6 +116,7 @@ const ( JobStatusTimedOut JobStatus = 4 JobStatusFailed JobStatus = 5 JobStatusIngesting JobStatus = 6 + JobStatusAnalyzing JobStatus = 7 ) func allJobStatuses() []JobStatus { @@ -128,6 +129,7 @@ func allJobStatuses() []JobStatus { JobStatusTimedOut, JobStatusFailed, JobStatusIngesting, + JobStatusAnalyzing, } } @@ -166,6 +168,9 @@ func (s JobStatus) String() string { case JobStatusIngesting: return "INGESTING" + case JobStatusAnalyzing: + return "ANALYZING" + default: return "INVALIDSTATUS" } diff --git a/cmd/api/src/services/fileupload/file_upload.go b/cmd/api/src/services/fileupload/file_upload.go index f1b7769cb5..6517547321 100644 --- a/cmd/api/src/services/fileupload/file_upload.go +++ b/cmd/api/src/services/fileupload/file_upload.go @@ -110,29 +110,22 @@ func TouchFileUploadJobLastIngest(db FileUploadData, fileUploadJob model.FileUpl return db.UpdateFileUploadJob(fileUploadJob) } -func EndFileUploadJob(db FileUploadData, job model.FileUploadJob) (model.FileUploadJob, error) { +func EndFileUploadJob(db FileUploadData, job model.FileUploadJob) error { job.Status = model.JobStatusIngesting + if err := db.UpdateFileUploadJob(job); err != nil { - return job, fmt.Errorf("error ending file upload job: %w", err) - } else { - return job, nil + return fmt.Errorf("error ending file upload job: %w", err) } -} -func UpdateFileUploadJobStatus(db FileUploadData, jobID int64, status model.JobStatus, message string) error { - if job, err := db.GetFileUploadJob(jobID); err != nil { - return err - } else { - job.Status = status - job.StatusMessage = message - job.EndTime = time.Now().UTC() - - return db.UpdateFileUploadJob(job) - } + return nil } -func CompleteFileUploadJob(db FileUploadData, jobID int64) (model.FileUploadJob, error) { - return model.FileUploadJob{}, nil +func UpdateFileUploadJobStatus(db FileUploadData, fileUploadJob model.FileUploadJob, status model.JobStatus, message string) error { + fileUploadJob.Status = status + fileUploadJob.StatusMessage = message + fileUploadJob.EndTime = time.Now().UTC() + + return db.UpdateFileUploadJob(fileUploadJob) } func TimeOutUploadJob(db FileUploadData, jobID int64, message string) error { diff --git a/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts b/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts index 677a7a9e2c..0e7f785072 100644 --- a/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts +++ b/packages/javascript/bh-shared-ui/src/components/FinishedIngestLog/types.ts @@ -34,7 +34,9 @@ export enum FileUploadJobStatus { TIMED_OUT = 4, FAILED = 5, INGESTING = 6, + ANALYZING = 7, } + export const FileUploadJobStatusToString: Record = { [-1]: 'Invalid', 0: 'Ready', @@ -44,4 +46,5 @@ export const FileUploadJobStatusToString: Record = 4: 'Timed Out', 5: 'Failed', 6: 'Ingesting', + 7: 'Analyzing', };