From 18b69e6dc3f8db9a886261e7077cbb1ee26c4c4d Mon Sep 17 00:00:00 2001 From: Ulises Rangel Date: Tue, 26 Mar 2024 12:31:27 -0500 Subject: [PATCH] chore: gate graph clearing (#523) * chore: gate graph clearing * chore: use feature flag for gating * chore: update error messages * chore: conform flag key * chore: mock feature flag response in test * chore: enable flag by default, update UI tests * chore: make flag non user updatable * chore: handle error earlier in if/else chain, return early on error * chore: mock expected call to getFlagByKey in dbwipe tests --- cmd/api/src/api/v2/database_wipe.go | 22 +++++++++++++-- cmd/api/src/api/v2/database_wipe_test.go | 8 ++++++ cmd/api/src/model/appcfg/flag.go | 9 ++++++ .../DatabaseManagement.test.tsx | 28 +++++++++++++++---- .../DatabaseManagement/DatabaseManagement.tsx | 20 ++++++++----- 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/cmd/api/src/api/v2/database_wipe.go b/cmd/api/src/api/v2/database_wipe.go index 9103ff9b3..645cd6819 100644 --- a/cmd/api/src/api/v2/database_wipe.go +++ b/cmd/api/src/api/v2/database_wipe.go @@ -26,6 +26,7 @@ import ( "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/model" + "github.com/specterops/bloodhound/src/model/appcfg" ) type DatabaseWipe struct { @@ -95,8 +96,25 @@ func (s Resources) HandleDatabaseWipe(response http.ResponseWriter, request *htt // delete graph if payload.DeleteCollectedGraphData { - s.TaskNotifier.RequestDeletion() - s.handleAuditLogForDatabaseWipe(request.Context(), auditEntry, true, "collected graph data") + if clearGraphDataFlag, err := s.DB.GetFlagByKey(request.Context(), appcfg.FeatureClearGraphData); err != nil { + api.WriteErrorResponse( + request.Context(), + api.BuildErrorResponse(http.StatusInternalServerError, "unable to inspect the feature flag for clearing graph data", request), + response, + ) + return + } else if !clearGraphDataFlag.Enabled { + api.WriteErrorResponse( + request.Context(), + api.BuildErrorResponse(http.StatusBadRequest, "deleting graph data is currently disabled", request), + response, + ) + return + } else { + s.TaskNotifier.RequestDeletion() + s.handleAuditLogForDatabaseWipe(request.Context(), auditEntry, true, "collected graph data") + } + } // delete asset group selectors diff --git a/cmd/api/src/api/v2/database_wipe_test.go b/cmd/api/src/api/v2/database_wipe_test.go index 785e25892..4cbf81210 100644 --- a/cmd/api/src/api/v2/database_wipe_test.go +++ b/cmd/api/src/api/v2/database_wipe_test.go @@ -28,6 +28,7 @@ import ( "github.com/specterops/bloodhound/src/api/v2/apitest" taskerMocks "github.com/specterops/bloodhound/src/daemons/datapipe/mocks" dbMocks "github.com/specterops/bloodhound/src/database/mocks" + "github.com/specterops/bloodhound/src/model/appcfg" "go.uber.org/mock/gomock" ) @@ -86,6 +87,10 @@ func TestDatabaseWipe(t *testing.T) { apitest.BodyStruct(input, v2.DatabaseWipe{DeleteCollectedGraphData: true}) }, Setup: func() { + mockDB.EXPECT().GetFlagByKey(gomock.Any(), gomock.Any()).Return(appcfg.FeatureFlag{ + Enabled: true, + }, nil) + taskerIntent := mockTasker.EXPECT().RequestDeletion().Times(1) successfulAuditLogIntent := mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any()).Return(nil).Times(1) successfulAuditLogWipe := mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any()).Return(nil).Times(1) @@ -247,6 +252,9 @@ func TestDatabaseWipe(t *testing.T) { successfulAuditLogIntent := mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any()).Return(nil).Times(1) // collected graph data operations + mockDB.EXPECT().GetFlagByKey(gomock.Any(), gomock.Any()).Return(appcfg.FeatureFlag{ + Enabled: true, + }, nil) taskerIntent := mockTasker.EXPECT().RequestDeletion().Times(1) nodesDeletedAuditLog := mockDB.EXPECT().AppendAuditLog(gomock.Any(), gomock.Any()).Return(nil).Times(1) gomock.InOrder(successfulAuditLogIntent, taskerIntent, nodesDeletedAuditLog) diff --git a/cmd/api/src/model/appcfg/flag.go b/cmd/api/src/model/appcfg/flag.go index 711189208..397d09e5e 100644 --- a/cmd/api/src/model/appcfg/flag.go +++ b/cmd/api/src/model/appcfg/flag.go @@ -18,6 +18,7 @@ package appcfg import ( "context" + "github.com/specterops/bloodhound/src/model" ) @@ -29,6 +30,7 @@ const ( FeatureReconciliation = "reconciliation" FeatureEntityPanelCaching = "entity_panel_cache" FeatureAdcs = "adcs" + FeatureClearGraphData = "clear_graph_data" ) // AvailableFlags returns a FeatureFlagSet of expected feature flags. Feature flag defaults introduced here will become the initial @@ -84,6 +86,13 @@ func AvailableFlags() FeatureFlagSet { Enabled: false, UserUpdatable: false, }, + FeatureClearGraphData: { + Key: FeatureClearGraphData, + Name: "Clear Graph Data", + Description: "Enables the ability to delete all nodes and edges from the graph database.", + Enabled: true, + UserUpdatable: false, + }, } } diff --git a/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.test.tsx b/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.test.tsx index 359023d43..a4f2a397e 100644 --- a/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.test.tsx +++ b/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.test.tsx @@ -24,6 +24,22 @@ describe('DatabaseManagement', () => { const server = setupServer( rest.post('/api/v2/clear-database', (req, res, ctx) => { return res(ctx.status(204)); + }), + rest.get('/api/v2/features', async (req, res, ctx) => { + return res( + ctx.json({ + data: [ + { + id: 1, + key: 'clear_graph_data', + name: 'Clear Graph Data', + description: 'Enables the ability to delete all nodes and edges from the graph database.', + enabled: true, + user_updatable: true, + }, + ], + }) + ); }) ); @@ -35,12 +51,14 @@ describe('DatabaseManagement', () => { afterEach(() => server.resetHandlers()); afterAll(() => server.close()); - it('renders', () => { + it('renders', async () => { const title = screen.getByText(/clear bloodhound data/i); - const checkboxes = screen.getAllByRole('checkbox'); const button = screen.getByRole('button', { name: /proceed/i }); expect(title).toBeInTheDocument(); + expect(await screen.findByRole('checkbox', { name: /Collected graph data/i })).toBeInTheDocument(); + + const checkboxes = screen.getAllByRole('checkbox'); expect(checkboxes.length).toEqual(5); expect(button).toBeInTheDocument(); }); @@ -64,7 +82,7 @@ describe('DatabaseManagement', () => { const errorMsg = screen.getByText(/please make a selection/i); expect(errorMsg).toBeInTheDocument(); - const checkbox = screen.getByRole('checkbox', { name: /collected graph data/i }); + const checkbox = screen.getByRole('checkbox', { name: /All asset group selectors/i }); await user.click(checkbox); expect(errorMsg).not.toBeInTheDocument(); @@ -73,7 +91,7 @@ describe('DatabaseManagement', () => { it('open and closes dialog', async () => { const user = userEvent.setup(); - const checkbox = screen.getByRole('checkbox', { name: /collected graph data/i }); + const checkbox = screen.getByRole('checkbox', { name: /All asset group selectors/i }); await user.click(checkbox); const button = screen.getByRole('button', { name: /proceed/i }); @@ -91,7 +109,7 @@ describe('DatabaseManagement', () => { it('handles posting a mutation', async () => { const user = userEvent.setup(); - const checkbox = screen.getByRole('checkbox', { name: /collected graph data/i }); + const checkbox = screen.getByRole('checkbox', { name: /All asset group selectors/i }); await user.click(checkbox); const proceedButton = screen.getByRole('button', { name: /proceed/i }); diff --git a/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.tsx b/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.tsx index 01e3db8aa..efe3a8bd6 100644 --- a/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.tsx +++ b/cmd/ui/src/views/DatabaseManagement/DatabaseManagement.tsx @@ -22,6 +22,7 @@ import { useMutation } from 'react-query'; import { useSelector } from 'react-redux'; import { selectAllAssetGroupIds, selectTierZeroAssetGroupId } from 'src/ducks/assetgroups/reducer'; import { ClearDatabaseRequest } from 'js-client-library'; +import FeatureFlag from 'src/components/FeatureFlag'; const initialState: State = { deleteCollectedGraphData: false, @@ -249,13 +250,18 @@ const DatabaseManagement = () => { ) : null} - + } /> } />