Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions cmd/api/src/api/v2/database_wipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ import (
"github.com/specterops/bloodhound/cmd/api/src/model"
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
"github.com/specterops/dawgs/graph"
"github.com/specterops/dawgs/ops"
"github.com/specterops/dawgs/query"
)

type DatabaseWipe struct {
DeleteCollectedGraphData bool `json:"deleteCollectedGraphData"`
DeleteSourceKinds []int `json:"deleteSourceKinds"` // an id of 0 represents "sourceless" data
DeleteCollectedGraphData bool `json:"deleteCollectedGraphData"`
DeleteSourceKinds []int `json:"deleteSourceKinds"` // an id of 0 represents "sourceless" data
DeleteRelationships []string `json:"deleteRelationships"`

DeleteFileIngestHistory bool `json:"deleteFileIngestHistory"`
DeleteDataQualityHistory bool `json:"deleteDataQualityHistory"`
Expand Down Expand Up @@ -63,7 +66,7 @@ func (s Resources) HandleDatabaseWipe(response http.ResponseWriter, request *htt
}

// return `BadRequest` if request is empty
isEmptyRequest := !payload.DeleteCollectedGraphData && !payload.DeleteDataQualityHistory && !payload.DeleteFileIngestHistory && len(payload.DeleteAssetGroupSelectors) == 0 && len(payload.DeleteSourceKinds) == 0
isEmptyRequest := !payload.DeleteCollectedGraphData && !payload.DeleteDataQualityHistory && !payload.DeleteFileIngestHistory && len(payload.DeleteRelationships) == 0 && len(payload.DeleteAssetGroupSelectors) == 0 && len(payload.DeleteSourceKinds) == 0
if isEmptyRequest {
api.WriteErrorResponse(
request.Context(),
Expand Down Expand Up @@ -188,6 +191,13 @@ func (s Resources) HandleDatabaseWipe(response http.ResponseWriter, request *htt
}
}

// delete requested graph edges by name
if len(payload.DeleteRelationships) > 0 {
if failure := s.deleteEdges(request.Context(), &auditEntry, payload.DeleteRelationships); failure {
errors = append(errors, "graph edges")
}
}

// return a user-friendly error message indicating what operations failed
if len(errors) > 0 {
api.WriteErrorResponse(
Expand Down Expand Up @@ -237,6 +247,35 @@ func (s Resources) deleteDataQualityHistory(ctx context.Context, auditEntry *mod
}
}

func (s Resources) deleteEdges(ctx context.Context, auditEntry *model.AuditEntry, edgeNames []string) (failure bool) {
// Use the graph batch API to find and delete relationships matching the provided edge names
if err := s.Graph.BatchOperation(ctx, func(batch graph.Batch) error {
for _, name := range edgeNames {
targetCriteria := query.Kind(query.Relationship(), graph.StringKind(name))

rels, err := ops.FetchRelationships(batch.Relationships().Filter(targetCriteria))
if err != nil {
return err
}

for _, rel := range rels {
if err := batch.DeleteRelationship(rel.ID); err != nil {
return err
}
}
}

return nil
}); err != nil {
slog.ErrorContext(ctx, fmt.Sprintf("%s: %s", "there was an error deleting graph edges", err.Error()))
s.handleAuditLogForDatabaseWipe(ctx, auditEntry, false, strings.Join(edgeNames, ", "))
return true
} else {
s.handleAuditLogForDatabaseWipe(ctx, auditEntry, true, strings.Join(edgeNames, ", "))
return false
}
}
Comment on lines +250 to +277
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for documentation or examples of batch relationship deletion patterns
# to understand if there are memory-efficient alternatives

# Look for other uses of ops.FetchRelationships to see if pagination is used
rg -n -C3 -A5 'ops\.FetchRelationships' --type=go

# Look for batch deletion patterns in the codebase
rg -n -C3 'batch\.DeleteRelationship' --type=go

# Check if there are examples of validating relationship kinds
rg -n -C3 'FetchKinds.*Relationship' --type=go

Repository: SpecterOps/BloodHound

Length of output: 23132


🏁 Script executed:

# Also look for how edgeNames parameter might be validated or constrained
rg -n -C3 'deleteEdges' --type=go

# Look for streaming or pagination patterns in batch operations
rg -n -C3 'batch.*Stream\|batch.*Pagina' --type=go

# Check the Graph.BatchOperation signature and documentation
rg -n -C5 'BatchOperation' --type=go | head -50

Repository: SpecterOps/BloodHound

Length of output: 4650


🏁 Script executed:

# Search for validation of relationship/edge types in the codebase
rg -n -C3 'Relationship.*Kind\|Kind.*Relationship' --type=go

# Look for any TODO/FIXME comments related to memory or validation
rg -n 'TODO|FIXME' cmd/api/src/api/v2/database_wipe.go

Repository: SpecterOps/BloodHound

Length of output: 47


🏁 Script executed:

# Find where payload.DeleteRelationships is defined
rg -n -A5 'DeleteRelationships' --type=go cmd/api/src/api/v2/database_wipe.go

# Look for the payload structure definition
rg -n -B5 -A10 'type.*[Pp]ayload.*struct' cmd/api/src/api/v2/database_wipe.go

# Check for any validation of edge names in the function or nearby
rg -n -B10 'deleteEdges' cmd/api/src/api/v2/database_wipe.go | head -50

Repository: SpecterOps/BloodHound

Length of output: 2093


🏁 Script executed:

# Look for relationship kind definitions to understand valid edge types
fd -n '\.go$' packages/go/analysis | xargs rg -l 'Relationship.*=.*string' | head -5

# Check if there are constants defining valid relationship kinds
rg -n -B2 -A2 'const.*Relationship' packages/go/analysis/ad/ | head -50

Repository: SpecterOps/BloodHound

Length of output: 492


🏁 Script executed:

# Inspect the complete deleteEdges function and surrounding context
sed -n '190,280p' cmd/api/src/api/v2/database_wipe.go

Repository: SpecterOps/BloodHound

Length of output: 3505


🏁 Script executed:

# Check if there's a list of valid relationship types that could be validated against
fd -type f '\.go$' packages/go/graphschema | head -10

# Look for relationship kind constants or definitions
rg -n 'const.*=' packages/go/graphschema/ad/identifiers.go | grep -i 'relationship\|raw\|owns' | head -20

Repository: SpecterOps/BloodHound

Length of output: 317


🏁 Script executed:

# Check if there's any API documentation or swagger definition for this endpoint
fd -name '*database*wipe*' --type f

# Look for test cases to understand expected behavior
rg -n 'deleteEdges\|DeleteRelationships' --type=go -A10 -B5 | grep -i test

Repository: SpecterOps/BloodHound

Length of output: 295


🏁 Script executed:

# Find graphschema files
fd 'identifiers\.go' packages/go/

# Look for relationship definitions
rg 'const.*=' packages/go/graphschema/ | grep -i 'own\|raw' | head -20

Repository: SpecterOps/BloodHound

Length of output: 109


🏁 Script executed:

# Search for test cases related to deleteEdges or DeleteRelationships
rg 'deleteEdges|DeleteRelationships' --type=go -B3 -A8 | grep -A8 -B3 'Test\|test' | head -50

Repository: SpecterOps/BloodHound

Length of output: 47


🏁 Script executed:

# Look at the complete payload structure in database_wipe.go to understand all context
cat cmd/api/src/api/v2/database_wipe.go | grep -A50 'type.*Payload.*struct'

Repository: SpecterOps/BloodHound

Length of output: 47


🏁 Script executed:

# Get the payload struct definition
sed -n '30,50p' cmd/api/src/api/v2/database_wipe.go

# Look for any validation logic before deleteEdges is called
sed -n '50,200p' cmd/api/src/api/v2/database_wipe.go | head -80

Repository: SpecterOps/BloodHound

Length of output: 3537


🏁 Script executed:

# Search for where relationship kind constants might be defined
fd identifiers.go

# Look at the graphschema structure
ls -la packages/go/graphschema/

Repository: SpecterOps/BloodHound

Length of output: 643


🏁 Script executed:

# Check if there are any known valid relationship types or validation logic
rg 'valid.*Relationship|Relationship.*valid|KindIn' cmd/api/src/api/v2/ -A3 -B3

Repository: SpecterOps/BloodHound

Length of output: 8272


Add input validation for edge names against known relationship types and consider memory efficiency for large deletions.

The deleteEdges function has two issues:

  1. Missing input validation: Edge names bypass the validation pattern established elsewhere in the codebase (e.g., pathfinding.go validates against graph.Kinds(ad.Relationships()).Concatenate(azure.Relationships())). Invalid edge names will silently result in zero deleted relationships without alerting the user.

  2. Memory inefficiency: The function loads all matching relationships into memory via ops.FetchRelationships before deletion. This pattern is consistent throughout the codebase but lacks pagination or streaming alternatives. For large relationship sets, this creates unnecessary memory pressure.

Consider validating edge names against valid relationship types and exploring whether the batch API supports more efficient deletion patterns.


func (s Resources) handleAuditLogForDatabaseWipe(ctx context.Context, auditEntry *model.AuditEntry, success bool, msg string) {
if success {
auditEntry.Status = model.AuditLogStatusSuccess
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ describe('DatabaseManagement', () => {
// [ ] All asset group selectors
// [ ] File ingest log history
// [ ] Data quality
// [ ] HasSession edges
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes.length).toEqual(9);
expect(checkboxes.length).toEqual(10);
expect(deleteButton).toBeInTheDocument();
});

Expand Down
18 changes: 18 additions & 0 deletions cmd/ui/src/views/DatabaseManagement/DatabaseManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const initialState: State = {
deleteCustomHighValueSelectors: false,
deleteDataQualityHistory: false,
deleteFileIngestHistory: false,
deleteHasSessionEdges: false,
deleteSourceKinds: [],

noSelectionError: false,
Expand All @@ -55,6 +56,7 @@ type State = {
deleteCustomHighValueSelectors: boolean;
deleteDataQualityHistory: boolean;
deleteFileIngestHistory: boolean;
deleteHasSessionEdges: boolean;
deleteSourceKinds: number[];

// error state
Expand Down Expand Up @@ -102,6 +104,7 @@ const reducer = (state: State, action: Action): State => {
deleteCustomHighValueSelectors: false,
deleteDataQualityHistory: false,
deleteFileIngestHistory: false,
deleteHasSessionEdges: false,
deleteSourceKinds: [],

showSuccessMessage: true,
Expand Down Expand Up @@ -131,6 +134,7 @@ const reducer = (state: State, action: Action): State => {
state.deleteCustomHighValueSelectors,
state.deleteDataQualityHistory,
state.deleteFileIngestHistory,
state.deleteHasSessionEdges,
].filter(Boolean).length === 0 && state.deleteSourceKinds.length === 0;

if (noSelection) {
Expand Down Expand Up @@ -170,6 +174,7 @@ const useDatabaseManagement = () => {
deleteCustomHighValueSelectors,
deleteDataQualityHistory,
deleteFileIngestHistory,
deleteHasSessionEdges,
deleteSourceKinds,
} = state;

Expand Down Expand Up @@ -216,6 +221,7 @@ const useDatabaseManagement = () => {
deleteCollectedGraphData,
deleteDataQualityHistory,
deleteFileIngestHistory,
deleteRelationships: deleteHasSessionEdges ? ['HasSession'] : [],
deleteSourceKinds,
},
});
Expand Down Expand Up @@ -254,6 +260,7 @@ const DatabaseManagement: FC = () => {
deleteCustomHighValueSelectors,
deleteDataQualityHistory,
deleteFileIngestHistory,
deleteHasSessionEdges,
deleteSourceKinds,
} = state;

Expand Down Expand Up @@ -361,6 +368,17 @@ const DatabaseManagement: FC = () => {
/>
}
/>
<FormControlLabel
label="HasSession edges"
control={
<Checkbox
checked={deleteHasSessionEdges}
onChange={handleCheckbox}
name='deleteHasSessionEdges'
disabled={!hasPermission}
/>
}
/>
</FormGroup>
</FormControl>

Expand Down
8 changes: 7 additions & 1 deletion packages/go/openapi/doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -13316,7 +13316,7 @@
"post": {
"operationId": "DeleteBloodHoundDatabase",
"summary": "Delete your BloodHound data",
"description": "Wipes your BloodHound data permanently. Specify the data to delete in the request body. Possible data includes collected graph data, custom high value selectors, file ingest history, and data quality history.",
"description": "Wipes your BloodHound data permanently. Specify the data to delete in the request body. Possible data includes collected graph data, relationships of a specific type, custom high value selectors, file ingest history, and data quality history.",
"tags": [
"Database",
"Community",
Expand All @@ -13331,6 +13331,12 @@
"deleteCollectedGraphData": {
"type": "boolean"
},
"deleteRelationships": {
"type": "array",
"items": {
"type": "string"
}
},
"deleteFileIngestHistory": {
"type": "boolean"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ post:
summary: Delete your BloodHound data
description:
Wipes your BloodHound data permanently. Specify the data to delete in the request body.
Possible data includes collected graph data, custom high value selectors, file ingest history,
and data quality history.
Possible data includes collected graph data, relationships of specific types, custom high value selectors,
file ingest history, and data quality history.
tags:
- Database
- Community
Expand All @@ -35,6 +35,10 @@ post:
properties:
deleteCollectedGraphData:
type: boolean
deleteRelationships:
type: array
items:
type: string
deleteFileIngestHistory:
type: boolean
deleteDataQualityHistory:
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/js-client-library/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export interface ClearDatabaseRequest {
deleteAssetGroupSelectors: number[];
deleteCollectedGraphData: boolean;
deleteDataQualityHistory: boolean;
deleteRelationships?: string[];
deleteFileIngestHistory: boolean;
deleteSourceKinds: number[];
}
Expand Down
Loading