diff --git a/.github/config/integration.config.json b/.github/config/integration.config.json index 90a5973b3..bdfd7c58a 100644 --- a/.github/config/integration.config.json +++ b/.github/config/integration.config.json @@ -7,6 +7,8 @@ "collectors_base_path": "/tmp/collectors", "log_level": "ERROR", "log_path": "bhapi.log", + "enable_startup_wait_period": false, + "datapipe_interval": 1, "features": { "enable_auth": true }, diff --git a/cmd/api/src/api/error.go b/cmd/api/src/api/error.go index 8f074ed33..fa247c8cf 100644 --- a/cmd/api/src/api/error.go +++ b/cmd/api/src/api/error.go @@ -111,8 +111,6 @@ func BuildErrorResponse(httpStatus int, message string, request *http.Request) * // HandleDatabaseError writes an error (not found or other) depending on the database error encountered // Alternate: FormatDatabaseError() func HandleDatabaseError(request *http.Request, response http.ResponseWriter, err error) { - ctx.SetErrorContext(request, err) - if errors.Is(err, database.ErrNotFound) { WriteErrorResponse(request.Context(), BuildErrorResponse(http.StatusNotFound, ErrorResponseDetailsResourceNotFound, request), response) } else { diff --git a/cmd/api/src/api/middleware/logging.go b/cmd/api/src/api/middleware/logging.go index 0652c90fe..a78524d17 100644 --- a/cmd/api/src/api/middleware/logging.go +++ b/cmd/api/src/api/middleware/logging.go @@ -170,14 +170,6 @@ func LoggingMiddleware(cfg config.Configuration, idResolver auth.IdentityResolve logEvent.Int64("response_bytes", loggedResponse.bytesWritten) logEvent.Int("status", loggedResponse.statusCode) logEvent.Duration("elapsed", time.Since(requestContext.StartTime.UTC())) - - if requestContext.AuditCtx.Action != "" { - requestContext.AuditCtx.SetStatus(loggedResponse.statusCode) - - if err := db.AppendAuditLog(*requestContext, "", requestContext.AuditCtx.Model); err != nil { - log.Errorf("error writing to audit log: %s", err) - } - } }) } } diff --git a/cmd/api/src/api/v2/agi.go b/cmd/api/src/api/v2/agi.go index 76ed360f9..00c6b2d9a 100644 --- a/cmd/api/src/api/v2/agi.go +++ b/cmd/api/src/api/v2/agi.go @@ -178,8 +178,6 @@ func (s Resources) UpdateAssetGroup(response http.ResponseWriter, request *http. assetGroup model.AssetGroup ) - ctx.SetAuditContext(request, model.AuditContext{Action: "UpdateAssetGroup", Model: &assetGroup}) - if assetGroupID, err := strconv.Atoi(rawAssetGroupID); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if err := api.ReadJSONRequestPayloadLimited(&updateAssetGroupRequest, request); err != nil { @@ -200,11 +198,9 @@ func (s Resources) UpdateAssetGroup(response http.ResponseWriter, request *http. func (s Resources) CreateAssetGroup(response http.ResponseWriter, request *http.Request) { var createRequest CreateAssetGroupRequest - ctx.SetAuditContext(request, model.AuditContext{Action: "CreateAssetGroup", Model: &createRequest}) - if err := api.ReadJSONRequestPayloadLimited(&createRequest, request); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if newAssetGroup, err := s.DB.CreateAssetGroup(createRequest.Name, createRequest.Tag, false); err != nil { + } else if newAssetGroup, err := s.DB.CreateAssetGroup(request.Context(), createRequest.Name, createRequest.Tag, false); err != nil { api.HandleDatabaseError(request, response, err) } else { assetGroupURL := *ctx.Get(request.Context()).Host @@ -222,15 +218,13 @@ func (s Resources) DeleteAssetGroup(response http.ResponseWriter, request *http. assetGroup model.AssetGroup ) - ctx.SetAuditContext(request, model.AuditContext{Action: "DeleteAssetGroup", Model: &assetGroup}) - if assetGroupID, err := strconv.Atoi(rawAssetGroupID); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if assetGroup, err = s.DB.GetAssetGroup(int32(assetGroupID)); err != nil { api.HandleDatabaseError(request, response, err) } else if assetGroup.SystemGroup { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, "Cannot delete a system defined asset group.", request), response) - } else if err := s.DB.DeleteAssetGroup(assetGroup); err != nil { + } else if err := s.DB.DeleteAssetGroup(request.Context(), assetGroup); err != nil { api.HandleDatabaseError(request, response, err) } else { response.WriteHeader(http.StatusOK) @@ -282,8 +276,6 @@ func (s Resources) DeleteAssetGroupSelector(response http.ResponseWriter, reques rawAssetGroupSelectorID = pathVars[api.URIPathVariableAssetGroupSelectorID] ) - ctx.SetAuditContext(request, model.AuditContext{Action: "DeleteAssetGroupSelector", Model: &assetGroupSelector}) - if assetGroupID, err := strconv.Atoi(rawAssetGroupID); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if _, err := s.DB.GetAssetGroup(int32(assetGroupID)); err != nil { diff --git a/cmd/api/src/api/v2/agi_test.go b/cmd/api/src/api/v2/agi_test.go index eb9e24411..32d5f68c1 100644 --- a/cmd/api/src/api/v2/agi_test.go +++ b/cmd/api/src/api/v2/agi_test.go @@ -20,15 +20,16 @@ import ( "context" "encoding/json" "fmt" - "github.com/specterops/bloodhound/headers" - "github.com/specterops/bloodhound/mediatypes" - "github.com/specterops/bloodhound/src/auth" - "github.com/specterops/bloodhound/src/test/must" "net/http" "net/http/httptest" "net/url" "testing" + "github.com/specterops/bloodhound/headers" + "github.com/specterops/bloodhound/mediatypes" + "github.com/specterops/bloodhound/src/auth" + "github.com/specterops/bloodhound/src/test/must" + "github.com/gorilla/mux" "github.com/specterops/bloodhound/dawgs/graph" "github.com/specterops/bloodhound/errors" @@ -348,20 +349,7 @@ func TestResources_UpdateAssetGroup(t *testing.T) { Require(). ResponseStatusCode(http.StatusBadRequest) - // Audit Log fails - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("exploded")) - - requestTemplate. - WithURLPathVars(map[string]string{ - "asset_group_id": "1234", - }). - WithBody(v2.UpdateAssetGroupRequest{}). - OnHandlerFunc(resources.UpdateAssetGroup). - Require(). - ResponseStatusCode(http.StatusInternalServerError) - // GetAssetGroup DB fails - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, fmt.Errorf("exploded")) requestTemplate. @@ -374,7 +362,6 @@ func TestResources_UpdateAssetGroup(t *testing.T) { ResponseStatusCode(http.StatusInternalServerError) // UpdateAssetGroup DB fails - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) mockDB.EXPECT().UpdateAssetGroup(model.AssetGroup{}).Return(fmt.Errorf("exploded")) @@ -388,7 +375,6 @@ func TestResources_UpdateAssetGroup(t *testing.T) { ResponseStatusCode(http.StatusInternalServerError) // Success - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) mockDB.EXPECT().UpdateAssetGroup(model.AssetGroup{}).Return(nil) @@ -420,18 +406,8 @@ func TestResources_CreateAssetGroup(t *testing.T) { Require(). ResponseStatusCode(http.StatusBadRequest) - // Audit Log fails - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("exploded")) - - requestTemplate. - WithBody(v2.CreateAssetGroupRequest{}). - OnHandlerFunc(resources.CreateAssetGroup). - Require(). - ResponseStatusCode(http.StatusInternalServerError) - // Create DB Query fails - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().CreateAssetGroup(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.AssetGroup{}, fmt.Errorf("exploded")) + mockDB.EXPECT().CreateAssetGroup(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(model.AssetGroup{}, fmt.Errorf("exploded")) requestTemplate. WithBody(v2.CreateAssetGroupRequest{}). @@ -440,8 +416,7 @@ func TestResources_CreateAssetGroup(t *testing.T) { ResponseStatusCode(http.StatusInternalServerError) // Success - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().CreateAssetGroup(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.AssetGroup{}, nil) + mockDB.EXPECT().CreateAssetGroup(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(model.AssetGroup{}, nil) requestTemplate. WithContext(&ctx.Context{ @@ -730,18 +705,6 @@ func TestResources_DeleteAssetGroup(t *testing.T) { // GetAssetGroup DB fails mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, fmt.Errorf("exploded")) - requestTemplate. - WithURLPathVars(map[string]string{ - "asset_group_id": "1234", - }). - OnHandlerFunc(resources.DeleteAssetGroup). - Require(). - ResponseStatusCode(http.StatusInternalServerError) - - // Audit Log DB fails - mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("exploded")) - requestTemplate. WithURLPathVars(map[string]string{ "asset_group_id": "1234", @@ -752,8 +715,7 @@ func TestResources_DeleteAssetGroup(t *testing.T) { // DeleteAssetGroup DB fails mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().DeleteAssetGroup(model.AssetGroup{}).Return(fmt.Errorf("exploded")) + mockDB.EXPECT().DeleteAssetGroup(gomock.Any(), model.AssetGroup{}).Return(fmt.Errorf("exploded")) requestTemplate. WithURLPathVars(map[string]string{ @@ -765,8 +727,7 @@ func TestResources_DeleteAssetGroup(t *testing.T) { // Success mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().DeleteAssetGroup(model.AssetGroup{}).Return(nil) + mockDB.EXPECT().DeleteAssetGroup(gomock.Any(), model.AssetGroup{}).Return(nil) requestTemplate. WithURLPathVars(map[string]string{ @@ -847,24 +808,9 @@ func TestResources_DeleteAssetGroupSelector(t *testing.T) { Require(). ResponseStatusCode(http.StatusConflict) - // Audit Log DB fails - mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) - mockDB.EXPECT().GetAssetGroupSelector(int32(1234)).Return(model.AssetGroupSelector{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("exploded")) - - requestTemplate. - WithURLPathVars(map[string]string{ - "asset_group_id": "1234", - "asset_group_selector_id": "1234", - }). - OnHandlerFunc(resources.DeleteAssetGroupSelector). - Require(). - ResponseStatusCode(http.StatusInternalServerError) - // DeleteAssetGroupSelector DB fails mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) mockDB.EXPECT().GetAssetGroupSelector(int32(1234)).Return(model.AssetGroupSelector{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().DeleteAssetGroupSelector(model.AssetGroupSelector{}).Return(fmt.Errorf("exploded")) requestTemplate. @@ -879,7 +825,6 @@ func TestResources_DeleteAssetGroupSelector(t *testing.T) { // Success mockDB.EXPECT().GetAssetGroup(int32(1234)).Return(model.AssetGroup{}, nil) mockDB.EXPECT().GetAssetGroupSelector(int32(1234)).Return(model.AssetGroupSelector{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().DeleteAssetGroupSelector(model.AssetGroupSelector{}).Return(nil) requestTemplate. diff --git a/cmd/api/src/api/v2/audit_integration_test.go b/cmd/api/src/api/v2/audit_integration_test.go index e27d0f710..43b5988aa 100644 --- a/cmd/api/src/api/v2/audit_integration_test.go +++ b/cmd/api/src/api/v2/audit_integration_test.go @@ -57,11 +57,26 @@ func Test_ListAuditLogs(t *testing.T) { // Expect one audit log entry from the deletion auditLogs := testCtx.ListAuditLogs(deletionTimestamp, time.Now(), 0, 1000) - require.Equal(t, 1, len(auditLogs), "Expected only 1 audit log entry but saw %d", len(auditLogs)) + require.Equal(t, 2, len(auditLogs), "Expected exactly 2 audit log entries but saw %d", len(auditLogs)) + + // Make sure these two actions are from the same request + require.Equal(t, auditLogs[0].RequestID, auditLogs[1].RequestID) + + // Makes sure these two actions are from the same two phase commit + require.Equal(t, auditLogs[0].CommitID, auditLogs[1].CommitID) + + // Audit logs are in LIFO order + require.Equal(t, auditLogs[0].Status, "success") + require.Equal(t, auditLogs[1].Status, "intent") testCtx.AssetAuditLog(auditLogs[0], "DeleteAssetGroup", map[string]any{ "asset_group_name": newAssetGroup.Name, "asset_group_tag": newAssetGroup.Tag, }) + + testCtx.AssetAuditLog(auditLogs[1], "DeleteAssetGroup", map[string]any{ + "asset_group_name": newAssetGroup.Name, + "asset_group_tag": newAssetGroup.Tag, + }) }) } diff --git a/cmd/api/src/api/v2/auth/auth.go b/cmd/api/src/api/v2/auth/auth.go index 2837323c0..e9e881957 100644 --- a/cmd/api/src/api/v2/auth/auth.go +++ b/cmd/api/src/api/v2/auth/auth.go @@ -121,8 +121,6 @@ func (s ManagementResource) GetSAMLProvider(response http.ResponseWriter, reques func (s ManagementResource) CreateSAMLProviderMultipart(response http.ResponseWriter, request *http.Request) { var samlIdentityProvider model.SAMLProvider - ctx.SetAuditContext(request, model.AuditContext{Action: "CreateSAMLIdentityProvider", Model: &samlIdentityProvider}) - if err := request.ParseMultipartForm(api.DefaultAPIPayloadReadLimitBytes); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) } else if providerNames, hasProviderName := request.MultipartForm.Value["name"]; !hasProviderName { @@ -167,10 +165,7 @@ func (s ManagementResource) disassociateUsersFromSAMLProvider(request *http.Requ user.SAMLProvider = nil user.SAMLProviderID = null.NewInt32(0, false) - // TODO: complex audit log transform - if err := s.db.AppendAuditLog(*ctx.FromRequest(request), "RemoveSAMLProvider", user); err != nil { - return api.FormatDatabaseError(err) - } else if err := s.db.UpdateUser(user); err != nil { + if err := s.db.UpdateUser(request.Context(), user); err != nil { return api.FormatDatabaseError(err) } } @@ -185,8 +180,6 @@ func (s ManagementResource) DeleteSAMLProvider(response http.ResponseWriter, req requestContext = ctx.FromRequest(request) ) - ctx.SetAuditContext(request, model.AuditContext{Action: "DeleteSAMLProvider", Model: &identityProvider}) - if providerID, err := strconv.ParseInt(rawProviderID, 10, 32); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) } else if identityProvider, err = s.db.GetSAMLProvider(int32(providerID)); err != nil { @@ -197,7 +190,7 @@ func (s ManagementResource) DeleteSAMLProvider(response http.ResponseWriter, req api.HandleDatabaseError(request, response, err) } else if err := s.disassociateUsersFromSAMLProvider(request, providerUsers); err != nil { api.HandleDatabaseError(request, response, err) - } else if err := s.db.DeleteSAMLProvider(identityProvider); err != nil { + } else if err := s.db.DeleteSAMLProvider(request.Context(), identityProvider); err != nil { api.HandleDatabaseError(request, response, err) } else { api.WriteBasicResponse(request.Context(), v2.DeleteSAMLProviderResponse{ @@ -431,8 +424,6 @@ func (s ManagementResource) CreateUser(response http.ResponseWriter, request *ht userTemplate model.User ) - ctx.SetAuditContext(request, model.AuditContext{Action: "CreateUser", Model: &userTemplate}) - if err := api.ReadJSONRequestPayloadLimited(&createUserRequest, request); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) } else if len(createUserRequest.Roles) > 1 { @@ -494,8 +485,7 @@ func (s ManagementResource) CreateUser(response http.ResponseWriter, request *ht } func (s ManagementResource) updateUser(response http.ResponseWriter, request *http.Request, user model.User) { - ctx.SetAuditContext(request, model.AuditContext{Action: "UpdateUser", Model: &user}) - if err := s.db.UpdateUser(user); err != nil { + if err := s.db.UpdateUser(request.Context(), user); err != nil { api.HandleDatabaseError(request, response, err) } else { response.WriteHeader(http.StatusOK) @@ -503,17 +493,7 @@ func (s ManagementResource) updateUser(response http.ResponseWriter, request *ht } func (s ManagementResource) ensureUserHasNoAuthSecret(context ctx.Context, user model.User) error { - // TODO: Determine if we need to actually create an audit log here - // var auditCtx = model.AuditContext{ - // Action: "DeleteUserAuthSecret", - // Model: user, - // } - if user.AuthSecret != nil { - if err := s.db.AppendAuditLog(context, "DeleteUserAuthSecret", user); err != nil { - return api.FormatDatabaseError(err) - } - if err := s.db.DeleteAuthSecret(*user.AuthSecret); err != nil { return api.FormatDatabaseError(err) } else { @@ -572,8 +552,6 @@ func (s ManagementResource) UpdateUser(response http.ResponseWriter, request *ht api.HandleDatabaseError(request, response, err) } else if provider, err := s.db.GetSAMLProvider(samlProviderID); err != nil { api.HandleDatabaseError(request, response, err) - } else if err := s.db.AppendAuditLog(*ctx.FromRequest(request), "SetUserSAMLProvider", user); err != nil { // TODO: complex audit log transform - api.HandleDatabaseError(request, response, err) } else { // Ensure that the AuthSecret reference is nil and that the SAML provider is set user.AuthSecret = nil @@ -619,8 +597,6 @@ func (s ManagementResource) DeleteUser(response http.ResponseWriter, request *ht rawUserID = pathVars[api.URIPathVariableUserID] ) - ctx.SetAuditContext(request, model.AuditContext{Action: "DeleteUser", Model: &user}) - if userID, err := uuid.FromString(rawUserID); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if user, err = s.db.GetUser(userID); err != nil { @@ -660,8 +636,6 @@ func (s ManagementResource) PutUserAuthSecret(response http.ResponseWriter, requ rawUserID = pathVars[api.URIPathVariableUserID] ) - ctx.SetAuditContext(request, model.AuditContext{Action: "PutUserAuthSecret", Model: &authSecret}) - if userID, err := uuid.FromString(rawUserID); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if err := api.ReadJSONRequestPayloadLimited(&setUserSecretRequest, request); err != nil { @@ -700,7 +674,6 @@ func (s ManagementResource) PutUserAuthSecret(response http.ResponseWriter, requ func (s ManagementResource) ExpireUserAuthSecret(response http.ResponseWriter, request *http.Request) { var ( rawUserID = mux.Vars(request)[api.URIPathVariableUserID] - auditCtx = model.AuditContext{Action: "InvalidateUserAuthSecret"} ) if userID, err := uuid.FromString(rawUserID); err != nil { @@ -716,8 +689,6 @@ func (s ManagementResource) ExpireUserAuthSecret(response http.ResponseWriter, r if err := s.db.UpdateAuthSecret(*authSecret); err != nil { api.HandleDatabaseError(request, response, err) } else { - auditCtx.Model = model.AuditData{"user_id": targetUser.ID} - ctx.SetAuditContext(request, auditCtx) // NOTE: This "should" be a 204 since we're not returning a payload but am returning a 200 to retain // uniformity. response.WriteHeader(http.StatusOK) @@ -806,7 +777,6 @@ func (s ManagementResource) CreateAuthToken(response http.ResponseWriter, reques var ( createUserTokenRequest = v2.CreateUserToken{} bhCtx = ctx.FromRequest(request) - auditCtx = model.AuditContext{Action: "CreateAuthToken"} ) if user, isUser := auth.GetUserFromAuthCtx(bhCtx.AuthCtx); !isUser { @@ -822,8 +792,6 @@ func (s ManagementResource) CreateAuthToken(response http.ResponseWriter, reques } else if newAuthToken, err := s.db.CreateAuthToken(authToken); err != nil { api.HandleDatabaseError(request, response, err) } else { - auditCtx.Model = model.AuditData{"user_id": user.ID} - ctx.SetAuditContext(request, auditCtx) api.WriteBasicResponse(request.Context(), newAuthToken, http.StatusOK, response) } } @@ -848,7 +816,6 @@ func (s ManagementResource) DeleteAuthToken(response http.ResponseWriter, reques pathVars = mux.Vars(request) rawTokenID = pathVars[api.URIPathVariableTokenID] bhCtx = ctx.FromRequest(request) - auditCtx = model.AuditContext{Action: "DeleteAuthToken"} ) if user, isUser := auth.GetUserFromAuthCtx(bhCtx.AuthCtx); !isUser { @@ -863,8 +830,6 @@ func (s ManagementResource) DeleteAuthToken(response http.ResponseWriter, reques } else if err := s.db.DeleteAuthToken(token); err != nil { api.HandleDatabaseError(request, response, err) } else { - auditCtx.Model = model.AuditData{"user_id": user.ID.String(), "token_id": token.ID} - ctx.SetAuditContext(request, auditCtx) response.WriteHeader(http.StatusOK) } } diff --git a/cmd/api/src/api/v2/auth/auth_test.go b/cmd/api/src/api/v2/auth/auth_test.go index 5b525ef58..fd73fc75e 100644 --- a/cmd/api/src/api/v2/auth/auth_test.go +++ b/cmd/api/src/api/v2/auth/auth_test.go @@ -82,7 +82,6 @@ func TestManagementResource_PutUserAuthSecret(t *testing.T) { }), }, nil).Times(1) mockDB.EXPECT().CreateAuthSecret(gomock.Any()).Return(model.AuthSecret{}, nil).Times(1) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1) // Happy path test.Request(t). @@ -141,8 +140,7 @@ func TestManagementResource_EnableUserSAML(t *testing.T) { mockDB.EXPECT().GetUser(badUserID).Return(model.User{AuthSecret: &model.AuthSecret{}}, nil) mockDB.EXPECT().GetUser(goodUserID).Return(model.User{}, nil) mockDB.EXPECT().GetSAMLProvider(samlProviderID).Return(model.SAMLProvider{}, nil).Times(2) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(5) - mockDB.EXPECT().UpdateUser(gomock.Any()).Return(nil).Times(2) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Any()).Return(nil).Times(2) mockDB.EXPECT().DeleteAuthSecret(gomock.Any()).Return(nil) // Happy path @@ -206,12 +204,11 @@ func TestManagementResource_DeleteSAMLProvider(t *testing.T) { mockDB.EXPECT().GetSAMLProvider(goodSAMLProvider.ID).Return(goodSAMLProvider, nil) mockDB.EXPECT().GetSAMLProvider(samlProviderWithUsers.ID).Return(samlProviderWithUsers, nil) - mockDB.EXPECT().DeleteSAMLProvider(gomock.Eq(goodSAMLProvider)).Return(nil) - mockDB.EXPECT().DeleteSAMLProvider(gomock.Eq(samlProviderWithUsers)).Return(nil) + mockDB.EXPECT().DeleteSAMLProvider(gomock.Any(), gomock.Eq(goodSAMLProvider)).Return(nil) + mockDB.EXPECT().DeleteSAMLProvider(gomock.Any(), gomock.Eq(samlProviderWithUsers)).Return(nil) mockDB.EXPECT().GetSAMLProviderUsers(goodSAMLProvider.ID).Return(nil, nil) mockDB.EXPECT().GetSAMLProviderUsers(samlProviderWithUsers.ID).Return(model.Users{samlEnabledUser}, nil) - mockDB.EXPECT().UpdateUser(gomock.Eq(samlEnabledUser)).Return(nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(3) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Eq(samlEnabledUser)).Return(nil) // Happy path test.Request(t). @@ -761,7 +758,6 @@ func TestExpireUserAuthSecret_Success(t *testing.T) { resources, mockDB := apitest.NewAuthManagementResource(mockCtrl) mockDB.EXPECT().GetUser(userId).Return(model.User{AuthSecret: &model.AuthSecret{}}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().UpdateAuthSecret(gomock.Any()).Return(nil) ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{}) @@ -1060,7 +1056,7 @@ func TestCreateUser_Failure(t *testing.T) { }, nil).AnyTimes() mockDB.EXPECT().GetRoles(badRole).Return(model.Roles{}, fmt.Errorf("db error")) mockDB.EXPECT().GetRoles(gomock.Not(badRole)).Return(model.Roles{}, nil).AnyTimes() - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mockDB.EXPECT().CreateUser(badUser).Return(model.User{}, fmt.Errorf("db error")) type Input struct { @@ -1175,7 +1171,6 @@ func TestCreateUser_Success(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{}) @@ -1229,7 +1224,6 @@ func TestCreateUser_ResetPassword(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil) input := struct { @@ -1303,7 +1297,6 @@ func TestManagementResource_UpdateUser_IDMalformed(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{}) @@ -1367,7 +1360,6 @@ func TestManagementResource_UpdateUser_GetUserError(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() mockDB.EXPECT().GetUser(gomock.Any()).Return(model.User{}, fmt.Errorf("foo")) @@ -1432,7 +1424,6 @@ func TestManagementResource_UpdateUser_GetRolesError(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() mockDB.EXPECT().GetUser(gomock.Any()).Return(goodUser, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, fmt.Errorf("foo")) @@ -1491,7 +1482,6 @@ func TestManagementResource_UpdateUser_SelfDisable(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() mockDB.EXPECT().GetUser(gomock.Any()).Return(goodUser, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{model.Role{ @@ -1573,7 +1563,6 @@ func TestManagementResource_UpdateUser_LookupActiveSessionsError(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() mockDB.EXPECT().GetUser(gomock.Any()).Return(goodUser, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{model.Role{ @@ -1631,87 +1620,6 @@ func TestManagementResource_UpdateUser_LookupActiveSessionsError(t *testing.T) { require.Contains(t, response.Body.String(), api.ErrorResponseDetailsInternalServerError) } -func TestManagementResource_UpdateUser_AppendAuditLogError(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - - endpoint := "/api/v2/auth/users" - - goodUserID, err := uuid.NewV4() - require.Nil(t, err) - - goodUser := model.User{ - PrincipalName: "good user", - Unique: model.Unique{ - ID: goodUserID, - }, - } - - resources, mockDB := apitest.NewAuthManagementResource(mockCtrl) - mockDB.EXPECT().GetConfigurationParameter(appcfg.PasswordExpirationWindow).Return(appcfg.Parameter{ - Key: appcfg.PasswordExpirationWindow, - Value: must.NewJSONBObject(appcfg.PasswordExpiration{ - Duration: appcfg.DefaultPasswordExpirationWindow, - }), - }, nil) - mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() - mockDB.EXPECT().GetUser(gomock.Any()).Return(goodUser, nil) - mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{model.Role{ - Name: "admin", - Description: "admin", - Permissions: model.Permissions{model.Permission{ - Authority: "admin", - Name: "admin", - Serial: model.Serial{}, - }}, - Serial: model.Serial{}, - }}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("foo")) - - ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{}) - input := v2.CreateUserRequest{ - UpdateUserRequest: v2.UpdateUserRequest{ - Principal: "good user", - }, - SetUserSecretRequest: v2.SetUserSecretRequest{ - Secret: "abcDEF123456$$", - NeedsPasswordReset: true, - }, - } - - payload, err := json.Marshal(input) - require.Nil(t, err) - req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - router := mux.NewRouter() - router.HandleFunc(endpoint, resources.CreateUser).Methods("POST") - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, rr.Code, http.StatusOK) - require.Contains(t, rr.Body.String(), "good user") - - userID, err := uuid.NewV4() - require.Nil(t, err) - - payload, err = json.Marshal(v2.UpdateUserRequest{}) - require.Nil(t, err) - - endpoint = fmt.Sprintf("/api/v2/bloodhound-users/%v", userID) - req, err = http.NewRequestWithContext(ctx, "PATCH", endpoint, bytes.NewReader(payload)) - require.Nil(t, err) - - req = mux.SetURLVars(req, map[string]string{api.URIPathVariableUserID: userID.String()}) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - response := httptest.NewRecorder() - handler := http.HandlerFunc(resources.UpdateUser) - handler.ServeHTTP(response, req) - require.Equal(t, http.StatusInternalServerError, response.Code) -} - func TestManagementResource_UpdateUser_DBError(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -1736,7 +1644,6 @@ func TestManagementResource_UpdateUser_DBError(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() mockDB.EXPECT().GetUser(gomock.Any()).Return(goodUser, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{model.Role{ @@ -1749,8 +1656,7 @@ func TestManagementResource_UpdateUser_DBError(t *testing.T) { }}, Serial: model.Serial{}, }}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().UpdateUser(gomock.Any()).Return(fmt.Errorf("foo")) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Any()).Return(fmt.Errorf("foo")) ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{}) input := v2.CreateUserRequest{ @@ -1820,7 +1726,6 @@ func TestManagementResource_UpdateUser_Success(t *testing.T) { }), }, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) mockDB.EXPECT().CreateUser(gomock.Any()).Return(goodUser, nil).AnyTimes() mockDB.EXPECT().GetUser(gomock.Any()).Return(goodUser, nil) mockDB.EXPECT().GetRoles(gomock.Any()).Return(model.Roles{model.Role{ @@ -1834,8 +1739,7 @@ func TestManagementResource_UpdateUser_Success(t *testing.T) { Serial: model.Serial{}, }}, nil) mockDB.EXPECT().LookupActiveSessionsByUser(gomock.Any()).Return([]model.UserSession{}, nil) - mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - mockDB.EXPECT().UpdateUser(gomock.Any()).Return(nil) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Any()).Return(nil) ctx := context.WithValue(context.Background(), ctx.ValueKey, &ctx.Context{}) input := v2.CreateUserRequest{ diff --git a/cmd/api/src/api/v2/auth/login.go b/cmd/api/src/api/v2/auth/login.go index a73318e4b..322c72786 100644 --- a/cmd/api/src/api/v2/auth/login.go +++ b/cmd/api/src/api/v2/auth/login.go @@ -1,22 +1,23 @@ // 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 auth import ( + "context" "fmt" "net/http" "strings" @@ -73,7 +74,7 @@ func (s LoginResource) Login(response http.ResponseWriter, request *http.Request if err := api.ReadJSONRequestPayloadLimited(&loginRequest, request); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if err = s.patchEULAAcceptance(loginRequest.Username); err != nil { + } else if err = s.patchEULAAcceptance(request.Context(), loginRequest.Username); err != nil { api.HandleDatabaseError(request, response, err) } else { switch strings.ToLower(loginRequest.LoginMethod) { @@ -87,12 +88,12 @@ func (s LoginResource) Login(response http.ResponseWriter, request *http.Request } // EULA Acceptance does not pertain to Bloodhound Community Edition; this flag is used for Bloodhound Enterprise users. -func (s LoginResource) patchEULAAcceptance(username string) error { +func (s LoginResource) patchEULAAcceptance(ctx context.Context, username string) error { if user, err := s.db.LookupUser(username); err != nil { return err } else { user.EULAAccepted = true - if err = s.db.UpdateUser(user); err != nil { + if err = s.db.UpdateUser(ctx, user); err != nil { return err } } diff --git a/cmd/api/src/api/v2/auth/login_internal_test.go b/cmd/api/src/api/v2/auth/login_internal_test.go index 61df022a6..f468401d0 100644 --- a/cmd/api/src/api/v2/auth/login_internal_test.go +++ b/cmd/api/src/api/v2/auth/login_internal_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 auth @@ -89,7 +89,7 @@ func TestLoginFailure(t *testing.T) { mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req3).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, fmt.Errorf("db error")) mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req4).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, api.ErrUserDisabled) mockDB.EXPECT().LookupUser(gomock.Any()).Return(model.User{EULAAccepted: false}, nil).Times(5) - mockDB.EXPECT().UpdateUser(gomock.Any()).Return(nil).Times(5) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Any()).Return(nil).Times(5) resources := NewLoginResource(config.Configuration{}, mockAuthenticator, mockDB) @@ -211,7 +211,7 @@ func TestLoginSuccess(t *testing.T) { mockAuthenticator := api_mocks.NewMockAuthenticator(mockCtrl) mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), input).Return(api.LoginDetails{User: model.User{AuthSecret: &model.AuthSecret{}, EULAAccepted: true}, SessionToken: "imasessiontoken"}, nil) mockDB.EXPECT().LookupUser(gomock.Any()).Return(model.User{EULAAccepted: false}, nil) - mockDB.EXPECT().UpdateUser(gomock.Any()).Return(nil) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Any()).Return(nil) resources := NewLoginResource(config.Configuration{}, mockAuthenticator, mockDB) diff --git a/cmd/api/src/api/v2/auth/login_test.go b/cmd/api/src/api/v2/auth/login_test.go index 66dd6505d..a017049a0 100644 --- a/cmd/api/src/api/v2/auth/login_test.go +++ b/cmd/api/src/api/v2/auth/login_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 auth @@ -63,7 +63,7 @@ func TestLoginExpiry(t *testing.T) { mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req1).Return(api.LoginDetails{User: model.User{AuthSecret: &model.AuthSecret{ExpiresAt: time.Now().UTC().Add(time.Hour * 24)}, EULAAccepted: true}, SessionToken: "imasession"}, nil) mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req2).Return(api.LoginDetails{User: model.User{AuthSecret: &model.AuthSecret{ExpiresAt: time.Now().UTC().Add(time.Hour * 24 * -1)}, EULAAccepted: true}, SessionToken: "imasession"}, nil) mockDB.EXPECT().LookupUser(gomock.Any()).Return(model.User{EULAAccepted: false}, nil).Times(2) - mockDB.EXPECT().UpdateUser(gomock.Any()).Return(nil).Times(2) + mockDB.EXPECT().UpdateUser(gomock.Any(), gomock.Any()).Return(nil).Times(2) resources := NewLoginResource(config.Configuration{}, mockAuthenticator, mockDB) diff --git a/cmd/api/src/api/v2/integration/audit.go b/cmd/api/src/api/v2/integration/audit.go index b58149f26..90d8d6f57 100644 --- a/cmd/api/src/api/v2/integration/audit.go +++ b/cmd/api/src/api/v2/integration/audit.go @@ -20,6 +20,7 @@ import ( "time" "github.com/specterops/bloodhound/src/model" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,13 +38,13 @@ func (s *Context) ListAuditLogs(after, before time.Time, offset, limit int) mode } func (s *Context) AssetAuditLog(auditLog model.AuditLog, expectedAction string, expectedFields map[string]any) { - require.Equal(s.TestCtrl, auditLog.Action, expectedAction) + assert.Equal(s.TestCtrl, auditLog.Action, expectedAction) for expectedFieldName, expectedFieldValue := range expectedFields { actualFieldValue, hasField := auditLog.Fields[expectedFieldName] - require.True(s.TestCtrl, hasField) - require.Equal(s.TestCtrl, expectedFieldValue, actualFieldValue) + assert.True(s.TestCtrl, hasField) + assert.Equal(s.TestCtrl, expectedFieldValue, actualFieldValue) } } diff --git a/cmd/api/src/ctx/ctx.go b/cmd/api/src/ctx/ctx.go index 9ef0bf28c..51e59d3c8 100644 --- a/cmd/api/src/ctx/ctx.go +++ b/cmd/api/src/ctx/ctx.go @@ -25,8 +25,6 @@ import ( "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/auth" - "github.com/specterops/bloodhound/src/database/types" - "github.com/specterops/bloodhound/src/model" ) // Use our own type rather than a primitive to avoid collisions @@ -48,7 +46,6 @@ type Context struct { AuthCtx auth.Context Host *url.URL RequestIP string - AuditCtx model.AuditContext } func (s *Context) ConstructGoContext() context.Context { @@ -107,50 +104,40 @@ func SetRequestContext(request *http.Request, bhCtx *Context) *http.Request { return request.WithContext(newRequestContext) } -func SetAuditContext(request *http.Request, auditCtx model.AuditContext) { - bhCtx := Get(request.Context()) - bhCtx.AuditCtx = auditCtx - Set(request.Context(), bhCtx) -} - -func SetErrorContext(request *http.Request, err error) { - bhCtx := Get(request.Context()) - bhCtx.AuditCtx.ErrorMsg = err.Error() - Set(request.Context(), bhCtx) -} - const ( ErrAuthContextInvalid = errors.Error("auth context is invalid") ) -func NewAuditLogFromContext(ctx Context, idResolver auth.IdentityResolver) (model.AuditLog, error) { - if ctx.AuditCtx.Model == nil { - return model.AuditLog{}, fmt.Errorf("model cannot be nil when creating a new audit log") - } else if ctx.AuditCtx.Action != model.AuditStatusFailure && ctx.AuditCtx.Action != model.AuditStatusSuccess { - return model.AuditLog{}, fmt.Errorf("invalid action specified in audit log: %s", ctx.AuditCtx.Action) - } - authContext := ctx.AuthCtx - - if !authContext.Authenticated() { - return model.AuditLog{}, ErrAuthContextInvalid - } else if identity, err := idResolver.GetIdentity(ctx.AuthCtx); err != nil { - return model.AuditLog{}, ErrAuthContextInvalid - } else { - auditLog := model.AuditLog{ - ActorID: identity.ID.String(), - ActorName: identity.Name, - ActorEmail: identity.Email, - Action: ctx.AuditCtx.Action, - Fields: types.JSONUntypedObject(ctx.AuditCtx.Model.AuditData()), - RequestID: ctx.RequestID, - Source: ctx.RequestIP, - Status: ctx.AuditCtx.Status, - } - - if auditLog.Status == model.AuditStatusFailure { - auditLog.Fields["error"] = ctx.AuditCtx.ErrorMsg - } - - return auditLog, nil - } -} +// Audit Log Reference + +// func NewAuditLogFromContext(ctx Context, idResolver auth.IdentityResolver) (model.AuditLog, error) { +// if ctx.AuditCtx.Model == nil { +// return model.AuditLog{}, fmt.Errorf("model cannot be nil when creating a new audit log") +// } else if ctx.AuditCtx.Action != model.AuditStatusFailure && ctx.AuditCtx.Action != model.AuditStatusSuccess { +// return model.AuditLog{}, fmt.Errorf("invalid action specified in audit log: %s", ctx.AuditCtx.Action) +// } +// authContext := ctx.AuthCtx + +// if !authContext.Authenticated() { +// return model.AuditLog{}, ErrAuthContextInvalid +// } else if identity, err := idResolver.GetIdentity(ctx.AuthCtx); err != nil { +// return model.AuditLog{}, ErrAuthContextInvalid +// } else { +// auditLog := model.AuditLog{ +// ActorID: identity.ID.String(), +// ActorName: identity.Name, +// ActorEmail: identity.Email, +// Action: ctx.AuditCtx.Action, +// Fields: types.JSONUntypedObject(ctx.AuditCtx.Model.AuditData()), +// RequestID: ctx.RequestID, +// Source: ctx.RequestIP, +// Status: ctx.AuditCtx.Status, +// } + +// if auditLog.Status == model.AuditStatusFailure { +// auditLog.Fields["error"] = ctx.AuditCtx.ErrorMsg +// } + +// return auditLog, nil +// } +// } diff --git a/cmd/api/src/database/agi.go b/cmd/api/src/database/agi.go index 348746303..d3a748dd9 100644 --- a/cmd/api/src/database/agi.go +++ b/cmd/api/src/database/agi.go @@ -17,6 +17,7 @@ package database import ( + "context" "time" "gorm.io/gorm" @@ -26,22 +27,40 @@ import ( "github.com/specterops/bloodhound/src/model" ) -func (s *BloodhoundDB) CreateAssetGroup(name, tag string, systemGroup bool) (model.AssetGroup, error) { - assetGroup := model.AssetGroup{ - Name: name, - Tag: tag, - SystemGroup: systemGroup, - } +func (s *BloodhoundDB) CreateAssetGroup(ctx context.Context, name, tag string, systemGroup bool) (model.AssetGroup, error) { + var ( + assetGroup = model.AssetGroup{ + Name: name, + Tag: tag, + SystemGroup: systemGroup, + } - return assetGroup, CheckError(s.db.Create(&assetGroup)) + auditEntry = model.AuditEntry{ + Action: "CreateAssetGroup", + Model: assetGroup.AuditData(), + } + ) + + return assetGroup, s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error { + return CheckError(tx.Create(&assetGroup)) + }) } func (s *BloodhoundDB) UpdateAssetGroup(assetGroup model.AssetGroup) error { return CheckError(s.db.Save(&assetGroup)) } -func (s *BloodhoundDB) DeleteAssetGroup(assetGroup model.AssetGroup) error { - return CheckError(s.db.Delete(&assetGroup)) +func (s *BloodhoundDB) DeleteAssetGroup(ctx context.Context, assetGroup model.AssetGroup) error { + var ( + auditEntry = model.AuditEntry{ + Action: "DeleteAssetGroup", + Model: assetGroup.AuditData(), + } + ) + + return s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error { + return CheckError(tx.Delete(&assetGroup)) + }) } func (s *BloodhoundDB) GetAssetGroup(id int32) (model.AssetGroup, error) { @@ -217,12 +236,6 @@ func (s *BloodhoundDB) UpdateAssetGroupSelectors(ctx ctx.Context, assetGroup mod }) } } - - // TODO: complex audit log transform - // ctx.AuditCtx = model.AuditContext{Action: "UpdateAssetGroupSelectors", Model: selectorSpec} - // if err := s.AppendAuditLog(ctx, "", selectorSpec); err != nil { - // return err - // } } return nil diff --git a/cmd/api/src/database/audit.go b/cmd/api/src/database/audit.go index 077c6f73a..055dcacac 100644 --- a/cmd/api/src/database/audit.go +++ b/cmd/api/src/database/audit.go @@ -17,24 +17,55 @@ package database import ( + "context" + "database/sql" "fmt" "time" + "github.com/gofrs/uuid" + "github.com/specterops/bloodhound/errors" + "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" + "github.com/specterops/bloodhound/src/database/types" + "github.com/specterops/bloodhound/src/model" "gorm.io/gorm" +) - "github.com/specterops/bloodhound/src/model" +const ( + ErrAuthContextInvalid = errors.Error("auth context is invalid") ) -func (s *BloodhoundDB) CreateAuditLog(auditLog *model.AuditLog) error { - return CheckError(s.db.Create(&auditLog)) +func newAuditLog(context context.Context, entry model.AuditEntry, idResolver auth.IdentityResolver) (model.AuditLog, error) { + bheCtx := ctx.Get(context) + + auditLog := model.AuditLog{ + Action: entry.Action, + Fields: types.JSONUntypedObject(entry.Model.AuditData()), + RequestID: bheCtx.RequestID, + Source: bheCtx.RequestIP, + Status: string(entry.Status), + CommitID: entry.CommitID, + } + + authContext := bheCtx.AuthCtx + if !authContext.Authenticated() { + return auditLog, ErrAuthContextInvalid + } else if identity, err := idResolver.GetIdentity(bheCtx.AuthCtx); err != nil { + return auditLog, ErrAuthContextInvalid + } else { + auditLog.ActorID = identity.ID.String() + auditLog.ActorName = identity.Name + auditLog.ActorEmail = identity.Email + } + + return auditLog, nil } -func (s *BloodhoundDB) AppendAuditLog(context ctx.Context, unused string, model model.Auditable) error { - if auditLog, err := ctx.NewAuditLogFromContext(context, s.idResolver); err != nil { - return fmt.Errorf("error creating audit log from context: %w", err) +func (s *BloodhoundDB) AppendAuditLog(ctx context.Context, entry model.AuditEntry) error { + if auditLog, err := newAuditLog(ctx, entry, s.idResolver); err != nil && err != ErrAuthContextInvalid { + return fmt.Errorf("audit log append: %w", err) } else { - return s.CreateAuditLog(&auditLog) + return CheckError(s.db.Create(&auditLog)) } } @@ -71,3 +102,34 @@ func (s *BloodhoundDB) ListAuditLogs(before, after time.Time, offset, limit int, return auditLogs, int(count), CheckError(result) } + +func (s *BloodhoundDB) AuditableTransaction(ctx context.Context, auditEntry model.AuditEntry, f func(tx *gorm.DB) error, opts ...*sql.TxOptions) error { + var ( + commitID, err = uuid.NewV4() + ) + + if err != nil { + return fmt.Errorf("commitID could not be created: %w", err) + } + + auditEntry.CommitID = commitID + auditEntry.Status = model.AuditStatusIntent + + if err := s.AppendAuditLog(ctx, auditEntry); err != nil { + return fmt.Errorf("could not append intent to audit log: %w", err) + } + + err = s.db.Transaction(f, opts...) + + if err != nil { + auditEntry.Status = model.AuditStatusFailure + } else { + auditEntry.Status = model.AuditStatusSuccess + } + + if err := s.AppendAuditLog(ctx, auditEntry); err != nil { + return fmt.Errorf("could not append end status to audit log: %w", err) + } + + return err +} diff --git a/cmd/api/src/database/audit_test.go b/cmd/api/src/database/audit_test.go index 683fe5c35..c1b33cf64 100644 --- a/cmd/api/src/database/audit_test.go +++ b/cmd/api/src/database/audit_test.go @@ -20,12 +20,14 @@ package database_test import ( + "context" + "testing" + "time" + "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/test/integration" - "testing" - "time" ) func TestDatabase_ListAuditLogs(t *testing.T) { @@ -53,7 +55,7 @@ func TestDatabase_ListAuditLogs(t *testing.T) { }, } for i := 0; i < 7; i++ { - if err := dbInst.AppendAuditLog(mockCtx, "CreateUser", model.User{}); err != nil { + if err := dbInst.AppendAuditLog(ctx.Set(context.Background(), &mockCtx), model.AuditEntry{Model: &model.User{}, Action: "CreateUser", Status: model.AuditStatusSuccess}); err != nil { t.Fatalf("Error creating audit log: %v", err) } } diff --git a/cmd/api/src/database/auth.go b/cmd/api/src/database/auth.go index 2a7768174..4f88e24e4 100644 --- a/cmd/api/src/database/auth.go +++ b/cmd/api/src/database/auth.go @@ -1,32 +1,33 @@ // 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 database import ( + "context" "crypto/rand" "encoding/base64" "fmt" "strings" "time" + "github.com/gofrs/uuid" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/model" - "github.com/gofrs/uuid" "gorm.io/gorm" ) @@ -360,14 +361,23 @@ func (s *BloodhoundDB) CreateUser(user model.User) (model.User, error) { // UpdateUser updates the roles associated with the user according to the input struct // UPDATE users SET roles = .... -func (s *BloodhoundDB) UpdateUser(user model.User) error { - // Update roles first - if err := s.db.Model(&user).Association("Roles").Replace(&user.Roles); err != nil { - return err - } +func (s *BloodhoundDB) UpdateUser(ctx context.Context, user model.User) error { + var ( + auditEntry = model.AuditEntry{ + Action: "UpdateUser", + Model: user.AuditData(), + } + ) - result := s.db.Save(&user) - return CheckError(result) + return s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error { + // Update roles first + if err := tx.Model(&user).Association("Roles").Replace(&user.Roles); err != nil { + return err + } + + result := tx.Save(&user) + return CheckError(result) + }) } func (s *BloodhoundDB) GetAllUsers(order string, filter model.SQLFilter) (model.Users, error) { @@ -597,8 +607,17 @@ func (s *BloodhoundDB) GetSAMLProvider(id int32) (model.SAMLProvider, error) { return samlProvider, CheckError(result) } -func (s *BloodhoundDB) DeleteSAMLProvider(provider model.SAMLProvider) error { - return CheckError(s.db.Delete(&provider)) +func (s *BloodhoundDB) DeleteSAMLProvider(ctx context.Context, provider model.SAMLProvider) error { + var ( + auditEntry = model.AuditEntry{ + Action: "DeleteSAMLProvider", + Model: provider.AuditData(), + } + ) + + return s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error { + return CheckError(tx.Delete(&provider)) + }) } // GetSAMLProviderUsers returns all users that are bound to the SAML provider ID provided diff --git a/cmd/api/src/database/auth_test.go b/cmd/api/src/database/auth_test.go index 93a3d4590..cb3bd0ff3 100644 --- a/cmd/api/src/database/auth_test.go +++ b/cmd/api/src/database/auth_test.go @@ -20,6 +20,7 @@ package database_test import ( + "context" "testing" "time" @@ -219,7 +220,7 @@ func TestDatabase_CreateGetUser(t *testing.T) { newUser.Roles = newUser.Roles.RemoveByName(roleToDelete) - if err := dbInst.UpdateUser(newUser); err != nil { + if err := dbInst.UpdateUser(context.Background(), newUser); err != nil { t.Fatalf("Failed to update user: %v", err) } @@ -328,7 +329,7 @@ func TestDatabase_CreateSAMLProvider(t *testing.T) { } else { user.SAMLProviderID = null.Int32From(newSAMLProvider.ID) - if err := dbInst.UpdateUser(user); err != nil { + if err := dbInst.UpdateUser(context.Background(), user); err != nil { t.Fatalf("Failed to update user: %v", err) } else if updatedUser, err := dbInst.GetUser(user.ID); err != nil { t.Fatalf("Failed to fetch updated user: %v", err) diff --git a/cmd/api/src/database/db.go b/cmd/api/src/database/db.go index 21bbe8de8..a6ab1d71d 100644 --- a/cmd/api/src/database/db.go +++ b/cmd/api/src/database/db.go @@ -19,6 +19,7 @@ package database //go:generate go run go.uber.org/mock/mockgen -copyright_file=../../../../LICENSE.header -destination=./mocks/db.go -package=mocks . Database import ( + "context" "fmt" "time" @@ -60,9 +61,9 @@ type Database interface { DeleteIngestTask(ingestTask model.IngestTask) error GetIngestTasksForJob(jobID int64) (model.IngestTasks, error) GetUnfinishedIngestIDs() ([]int64, error) - CreateAssetGroup(name, tag string, systemGroup bool) (model.AssetGroup, error) + CreateAssetGroup(ctx context.Context, name, tag string, systemGroup bool) (model.AssetGroup, error) UpdateAssetGroup(assetGroup model.AssetGroup) error - DeleteAssetGroup(assetGroup model.AssetGroup) error + DeleteAssetGroup(ctx context.Context, assetGroup model.AssetGroup) error GetAssetGroup(id int32) (model.AssetGroup, error) GetAllAssetGroups(order string, filter model.SQLFilter) (model.AssetGroups, error) SweepAssetGroupCollections() @@ -82,7 +83,7 @@ type Database interface { RawFirst(value any) error Wipe() error Migrate() error - AppendAuditLog(ctx ctx.Context, action string, data model.Auditable) error + AppendAuditLog(ctx context.Context, entry model.AuditEntry) error ListAuditLogs(before, after time.Time, offset, limit int, order string, filter model.SQLFilter) (model.AuditLogs, int, error) CreateRole(role model.Role) (model.Role, error) UpdateRole(role model.Role) error @@ -100,7 +101,7 @@ type Database interface { GetInstallation() (model.Installation, error) HasInstallation() (bool, error) CreateUser(user model.User) (model.User, error) - UpdateUser(user model.User) error + UpdateUser(ctx context.Context, user model.User) error GetAllUsers(order string, filter model.SQLFilter) (model.Users, error) GetUser(id uuid.UUID) (model.User, error) DeleteUser(user model.User) error @@ -122,7 +123,7 @@ type Database interface { GetAllSAMLProviders() (model.SAMLProviders, error) GetSAMLProvider(id int32) (model.SAMLProvider, error) GetSAMLProviderUsers(id int32) (model.Users, error) - DeleteSAMLProvider(samlProvider model.SAMLProvider) error + DeleteSAMLProvider(ctx context.Context, samlProvider model.SAMLProvider) error CreateUserSession(userSession model.UserSession) (model.UserSession, error) LookupActiveSessionsByUser(user model.User) ([]model.UserSession, error) EndUserSession(userSession model.UserSession) diff --git a/cmd/api/src/database/migration/migrations/v5.6.0.sql b/cmd/api/src/database/migration/migrations/v5.6.0.sql new file mode 100644 index 000000000..94ac5cf41 --- /dev/null +++ b/cmd/api/src/database/migration/migrations/v5.6.0.sql @@ -0,0 +1,6 @@ +ALTER TABLE IF EXISTS audit_logs +DROP CONSTRAINT IF EXISTS audit_logs_status_check, +ADD CONSTRAINT status_check +CHECK (status IN ('intent', 'success', 'failure')), +ALTER COLUMN status SET DEFAULT 'intent', +ADD COLUMN IF NOT EXISTS commit_id TEXT; \ No newline at end of file diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index 6e58a3d86..2d9049d5d 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -21,6 +21,7 @@ package mocks import ( + context "context" reflect "reflect" time "time" @@ -55,17 +56,17 @@ func (m *MockDatabase) EXPECT() *MockDatabaseMockRecorder { } // AppendAuditLog mocks base method. -func (m *MockDatabase) AppendAuditLog(arg0 ctx.Context, arg1 string, arg2 model.Auditable) error { +func (m *MockDatabase) AppendAuditLog(arg0 context.Context, arg1 model.AuditEntry) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AppendAuditLog", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "AppendAuditLog", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // AppendAuditLog indicates an expected call of AppendAuditLog. -func (mr *MockDatabaseMockRecorder) AppendAuditLog(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) AppendAuditLog(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendAuditLog", reflect.TypeOf((*MockDatabase)(nil).AppendAuditLog), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendAuditLog", reflect.TypeOf((*MockDatabase)(nil).AppendAuditLog), arg0, arg1) } // Close mocks base method. @@ -111,18 +112,18 @@ func (mr *MockDatabaseMockRecorder) CreateADDataQualityStats(arg0 interface{}) * } // CreateAssetGroup mocks base method. -func (m *MockDatabase) CreateAssetGroup(arg0, arg1 string, arg2 bool) (model.AssetGroup, error) { +func (m *MockDatabase) CreateAssetGroup(arg0 context.Context, arg1, arg2 string, arg3 bool) (model.AssetGroup, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateAssetGroup", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateAssetGroup", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(model.AssetGroup) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateAssetGroup indicates an expected call of CreateAssetGroup. -func (mr *MockDatabaseMockRecorder) CreateAssetGroup(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) CreateAssetGroup(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAssetGroup", reflect.TypeOf((*MockDatabase)(nil).CreateAssetGroup), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAssetGroup", reflect.TypeOf((*MockDatabase)(nil).CreateAssetGroup), arg0, arg1, arg2, arg3) } // CreateAssetGroupCollection mocks base method. @@ -365,17 +366,17 @@ func (mr *MockDatabaseMockRecorder) CreateUserSession(arg0 interface{}) *gomock. } // DeleteAssetGroup mocks base method. -func (m *MockDatabase) DeleteAssetGroup(arg0 model.AssetGroup) error { +func (m *MockDatabase) DeleteAssetGroup(arg0 context.Context, arg1 model.AssetGroup) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAssetGroup", arg0) + ret := m.ctrl.Call(m, "DeleteAssetGroup", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteAssetGroup indicates an expected call of DeleteAssetGroup. -func (mr *MockDatabaseMockRecorder) DeleteAssetGroup(arg0 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) DeleteAssetGroup(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAssetGroup", reflect.TypeOf((*MockDatabase)(nil).DeleteAssetGroup), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAssetGroup", reflect.TypeOf((*MockDatabase)(nil).DeleteAssetGroup), arg0, arg1) } // DeleteAssetGroupSelector mocks base method. @@ -435,17 +436,17 @@ func (mr *MockDatabaseMockRecorder) DeleteIngestTask(arg0 interface{}) *gomock.C } // DeleteSAMLProvider mocks base method. -func (m *MockDatabase) DeleteSAMLProvider(arg0 model.SAMLProvider) error { +func (m *MockDatabase) DeleteSAMLProvider(arg0 context.Context, arg1 model.SAMLProvider) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSAMLProvider", arg0) + ret := m.ctrl.Call(m, "DeleteSAMLProvider", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // DeleteSAMLProvider indicates an expected call of DeleteSAMLProvider. -func (mr *MockDatabaseMockRecorder) DeleteSAMLProvider(arg0 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) DeleteSAMLProvider(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSAMLProvider", reflect.TypeOf((*MockDatabase)(nil).DeleteSAMLProvider), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSAMLProvider", reflect.TypeOf((*MockDatabase)(nil).DeleteSAMLProvider), arg0, arg1) } // DeleteSavedQuery mocks base method. @@ -1484,17 +1485,17 @@ func (mr *MockDatabaseMockRecorder) UpdateSAMLIdentityProvider(arg0 interface{}) } // UpdateUser mocks base method. -func (m *MockDatabase) UpdateUser(arg0 model.User) error { +func (m *MockDatabase) UpdateUser(arg0 context.Context, arg1 model.User) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateUser", arg0) + ret := m.ctrl.Call(m, "UpdateUser", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // UpdateUser indicates an expected call of UpdateUser. -func (mr *MockDatabaseMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) UpdateUser(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockDatabase)(nil).UpdateUser), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockDatabase)(nil).UpdateUser), arg0, arg1) } // Wipe mocks base method. diff --git a/cmd/api/src/model/audit.go b/cmd/api/src/model/audit.go index 95730aac8..7f3b334a7 100644 --- a/cmd/api/src/model/audit.go +++ b/cmd/api/src/model/audit.go @@ -20,12 +20,16 @@ import ( "fmt" "time" + "github.com/gofrs/uuid" "github.com/specterops/bloodhound/src/database/types" ) +type AuditEntryStatus string + const ( - AuditStatusSuccess = "success" - AuditStatusFailure = "failure" + AuditStatusSuccess AuditEntryStatus = "success" + AuditStatusFailure AuditEntryStatus = "failure" + AuditStatusIntent AuditEntryStatus = "intent" ) type AuditLog struct { @@ -39,6 +43,7 @@ type AuditLog struct { RequestID string `json:"request_id"` Source string `json:"source"` Status string `json:"status"` + CommitID uuid.UUID `json:"commit_id" gorm:"type:text"` } func (s AuditLog) String() string { @@ -142,17 +147,10 @@ type Auditable interface { AuditData() AuditData } -type AuditContext struct { +type AuditEntry struct { + CommitID uuid.UUID Action string Model Auditable - Status string + Status AuditEntryStatus ErrorMsg string } - -func (s *AuditContext) SetStatus(statusCode int) { - if statusCode >= 200 && statusCode < 300 { - s.Status = AuditStatusSuccess - } else { - s.Status = AuditStatusFailure - } -} diff --git a/cmd/api/src/model/auth.go b/cmd/api/src/model/auth.go index df01dc5c9..77f8d7636 100644 --- a/cmd/api/src/model/auth.go +++ b/cmd/api/src/model/auth.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 model @@ -21,9 +21,9 @@ import ( "net/url" "time" + "github.com/gofrs/uuid" "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/serde" - "github.com/gofrs/uuid" ) const PermissionURIScheme = "permission" @@ -460,18 +460,13 @@ type User struct { Unique } -func (s User) AuditData() AuditData { - data := AuditData{ - "id": s.ID, - "principal_name": s.PrincipalName, - "roles": s.Roles.IDs(), - } - - if s.SAMLProviderID.Valid { - data["saml_provider_id"] = s.SAMLProviderID +func (s *User) AuditData() AuditData { + return AuditData{ + "id": s.ID, + "principal_name": s.PrincipalName, + "roles": s.Roles.IDs(), + "saml_provider_id": s.SAMLProviderID.ValueOrZero(), } - - return data } func (s *User) RemoveRole(role Role) { diff --git a/local-harnesses/build.config.json.template b/local-harnesses/build.config.json.template index 3556eaec5..4ba7a719c 100644 --- a/local-harnesses/build.config.json.template +++ b/local-harnesses/build.config.json.template @@ -7,6 +7,8 @@ "collectors_base_path": "/bhapi/collectors", "log_level": "INFO", "log_path": "bhapi.log", + "enable_startup_wait_period": false, + "datapipe_interval": 1, "features": { "enable_auth": true }, diff --git a/local-harnesses/integration.config.json.template b/local-harnesses/integration.config.json.template index 35b1713d2..9293aed87 100644 --- a/local-harnesses/integration.config.json.template +++ b/local-harnesses/integration.config.json.template @@ -7,6 +7,8 @@ "collectors_base_path": "/tmp/collectors", "log_level": "ERROR", "log_path": "bhapi.log", + "enable_startup_wait_period": false, + "datapipe_interval": 1, "features": { "enable_auth": true },