Skip to content

Commit 1e5969b

Browse files
resolve merge conflict
2 parents fa04f8d + 3944a1c commit 1e5969b

30 files changed

+1244
-254
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
# Copyright 2025 Specter Ops, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
# SPDX-License-Identifier: Apache-2.0
17+
name: Conventional Commit Formatting
18+
19+
on:
20+
pull_request:
21+
branches:
22+
- main
23+
- "stage/**"
24+
types:
25+
- opened
26+
- synchronize
27+
- edited
28+
29+
jobs:
30+
conventional-commit-check:
31+
name: Check conventional commit formatting
32+
uses: SpecterOps/BloodHound/.github/workflows/reusable.conventional-commits.yml@main
33+
secrets:
34+
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
35+
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
36+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Copyright 2025 Specter Ops, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# SPDX-License-Identifier: Apache-2.0
16+
17+
name: Conventional Commits Formatting
18+
19+
on:
20+
workflow_call:
21+
secrets:
22+
JIRA_BASE_URL:
23+
required: true
24+
JIRA_USER_EMAIL:
25+
required: true
26+
JIRA_API_TOKEN:
27+
required: true
28+
29+
jobs:
30+
check-commit-format:
31+
runs-on: self-hosted
32+
33+
steps:
34+
- uses: amannn/action-semantic-pull-request@v6
35+
id: lint_pr_title
36+
with:
37+
types: |
38+
feat
39+
fix
40+
docs
41+
refactor
42+
test
43+
chore
44+
headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*) (BED-\d+|PQE-\d+|#\d+)$'
45+
headerPatternCorrespondence: type, scope, subject, issue
46+
env:
47+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48+
49+
- uses: marocchino/sticky-pull-request-comment@v2
50+
if: always() && (steps.lint_pr_title.outputs.error_message != null)
51+
with:
52+
header: pr-title-lint-error
53+
message: |
54+
Howdy! Thank you for opening this pull request 🙇
55+
56+
It looks like your pull request title needs some adjustment.
57+
We require pull request titles to follow the Conventional Commits specification as outlined in our [documentation](https://github.com/SpecterOps/BloodHound/blob/main/rfc/bh-rfc-2.md).
58+
Please review the required format and update your pull request title accordingly.
59+
Thank you!
60+
61+
Details:
62+
63+
```
64+
${{ steps.lint_pr_title.outputs.error_message }}
65+
```
66+
67+
# Delete a previous comment when the issue has been resolved
68+
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
69+
uses: marocchino/sticky-pull-request-comment@v2
70+
with:
71+
header: pr-title-lint-error
72+
delete: true
73+
74+
- name: Check Issue Reference Exists
75+
id: "ref-check"
76+
env:
77+
JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
78+
JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
79+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
80+
run: |
81+
#!/usr/bin/env bash
82+
83+
set -euo pipefail
84+
85+
PR_TITLE="${{ github.event.pull_request.title }}"
86+
JIRA_ISSUE_REGEX=" (BED-[0-9]+|PQE-[0-9]+)$"
87+
GH_ISSUE_REGEX=" (#[0-9]+)$"
88+
89+
function check_jira_ref() {
90+
local REFERENCE=$1
91+
92+
echo "Jira issue reference found: ${REFERENCE}"
93+
94+
local URL="${{ secrets.JIRA_BASE_URL }}/rest/api/3/issue/${REFERENCE}"
95+
96+
echo "::debug::JIRA ISSUE URL: $URL"
97+
98+
# curl for issue info with jira api
99+
local ISSUE_RESPONSE=$(curl -u "${{ secrets.JIRA_USER_EMAIL }}:${{ secrets.JIRA_API_TOKEN }}" \
100+
-X GET \
101+
--url "$URL" \
102+
-H "Accept: application/json")
103+
104+
local HAS_ID=$(echo "$ISSUE_RESPONSE" | jq -e 'has("id")')
105+
106+
if ! $HAS_ID ; then
107+
ERROR="Issue request error: Invalid Jira issue"
108+
109+
echo $ERROR
110+
echo "ref_error=$ERROR" >> $GITHUB_OUTPUT
111+
112+
exit 1
113+
fi
114+
}
115+
116+
function check_github_ref() {
117+
local REFERENCE=$1
118+
119+
echo "GitHub Issue reference found: ${REFERENCE}"
120+
121+
local ISSUE_NUMBER="${REFERENCE:1}" # remove "#" from the start of the string
122+
123+
local URL="https://api.github.com/repos/SpecterOps/BloodHound/issues/${ISSUE_NUMBER}"
124+
125+
# curl for issue info with github api
126+
local ISSUE_RESPONSE=$(curl -L \
127+
-H "Accept: application/vnd.github+json" \
128+
-H "X-GitHub-Api-Version: 2022-11-28" \
129+
--url "$URL")
130+
131+
local IS_OPEN=$(echo "$ISSUE_RESPONSE" | jq -e 'if has("id") then .state == "open" else false end')
132+
133+
if ! $IS_OPEN ; then
134+
ERROR=$(echo "$ISSUE_RESPONSE" | jq -c)
135+
136+
echo "Issue request error: ${ERROR}"
137+
echo "ref_error=$ERROR" >> $GITHUB_OUTPUT
138+
139+
exit 1
140+
fi
141+
}
142+
143+
function main() {
144+
if [[ $PR_TITLE =~ $JIRA_ISSUE_REGEX ]]; then
145+
check_jira_ref "${BASH_REMATCH[1]}"
146+
elif [[ $PR_TITLE =~ $GH_ISSUE_REGEX ]]; then
147+
check_github_ref "${BASH_REMATCH[1]}"
148+
else
149+
local NO_REF_ERROR="No issue reference found in the title."
150+
echo "$NO_REF_ERROR"
151+
echo "ref_error=$NO_REF_ERROR" >> $GITHUB_OUTPUT
152+
exit 1
153+
fi
154+
}
155+
156+
main
157+
158+
- uses: marocchino/sticky-pull-request-comment@v2
159+
if: always() && (steps.ref-check.outputs.ref_error != null)
160+
with:
161+
header: pr-title-ref-check-error
162+
message: |
163+
Howdy! Thank you for opening this pull request 🙇
164+
165+
Your title is formatted correctly but we did not find a matching issue reference.
166+
Please verify that the reference is correct and available in Jira or GitHub issues.
167+
168+
Details:
169+
170+
```
171+
${{ steps.ref-check.outputs.ref_error }}
172+
```
173+
174+
# Delete a previous comment when the issue has been resolved
175+
- if: ${{ steps.ref-check.outputs.ref_error == null }}
176+
uses: marocchino/sticky-pull-request-comment@v2
177+
with:
178+
header: pr-title-lint-error
179+
delete: true

.yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.31-3831705a03-d8068b2f39.zip renamed to .yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.32-fc3ceed39d-f90e90290b.zip

168 KB
Binary file not shown.

cmd/api/src/api/v2/pathfinding.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/specterops/bloodhound/cmd/api/src/api"
2525
"github.com/specterops/bloodhound/cmd/api/src/api/bloodhoundgraph"
2626
"github.com/specterops/bloodhound/cmd/api/src/model"
27+
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
2728
"github.com/specterops/bloodhound/cmd/api/src/queries"
2829
"github.com/specterops/bloodhound/packages/go/graphschema/ad"
2930
"github.com/specterops/bloodhound/packages/go/graphschema/azure"
@@ -168,8 +169,9 @@ func (s *Resources) GetSearchResult(response http.ResponseWriter, request *http.
168169
searchType = strings.ToLower(searchTypeParameters[0])
169170
}
170171
}
171-
172-
if nodes, err := s.GraphQuery.SearchByNameOrObjectID(request.Context(), searchValue, searchType); err != nil {
172+
if openGraphSearchFeatureFlag, err := s.DB.GetFlagByKey(request.Context(), appcfg.FeatureOpenGraphSearch); err != nil {
173+
api.HandleDatabaseError(request, response, err)
174+
} else if nodes, err := s.GraphQuery.SearchByNameOrObjectID(request.Context(), openGraphSearchFeatureFlag.Enabled, searchValue, searchType); err != nil {
173175
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("Error getting search results: %v", err), request), response)
174176
} else {
175177
api.WriteBasicResponse(request.Context(), bloodhoundgraph.NodeSetToBloodHoundGraph(nodes), http.StatusOK, response)

cmd/api/src/api/v2/pathfinding_test.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import (
2424
"github.com/specterops/bloodhound/cmd/api/src/api"
2525
v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2"
2626
"github.com/specterops/bloodhound/cmd/api/src/api/v2/apitest"
27+
"github.com/specterops/bloodhound/cmd/api/src/database/mocks"
28+
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
29+
"github.com/specterops/bloodhound/cmd/api/src/queries"
2730
mocks_graph "github.com/specterops/bloodhound/cmd/api/src/queries/mocks"
2831
"github.com/specterops/bloodhound/packages/go/graphschema/ad"
2932
"github.com/specterops/dawgs/graph"
@@ -335,7 +338,8 @@ func TestResources_GetSearchResult(t *testing.T) {
335338
var (
336339
mockCtrl = gomock.NewController(t)
337340
mockGraph = mocks_graph.NewMockGraph(mockCtrl)
338-
resources = v2.Resources{GraphQuery: mockGraph}
341+
mockDB = mocks.NewMockDatabase(mockCtrl)
342+
resources = v2.Resources{GraphQuery: mockGraph, DB: mockDB}
339343
)
340344
defer mockCtrl.Finish()
341345

@@ -371,14 +375,32 @@ func TestResources_GetSearchResult(t *testing.T) {
371375
apitest.BodyContains(output, "Expected only one search type.")
372376
},
373377
},
378+
{
379+
Name: "FeatureFlagDatabaseError",
380+
Input: func(input *apitest.Input) {
381+
apitest.AddQueryParam(input, "query", "some query")
382+
},
383+
Setup: func() {
384+
mockDB.EXPECT().
385+
GetFlagByKey(gomock.Any(), appcfg.FeatureOpenGraphSearch).
386+
Return(appcfg.FeatureFlag{}, errors.New("database error"))
387+
},
388+
Test: func(output apitest.Output) {
389+
apitest.StatusCode(output, http.StatusInternalServerError)
390+
apitest.BodyContains(output, "an internal error has occurred that is preventing the service from servicing this request")
391+
},
392+
},
374393
{
375394
Name: "GraphDBSearchByNameOrObjectIDError",
376395
Input: func(input *apitest.Input) {
377396
apitest.AddQueryParam(input, "query", "some query")
378397
},
379398
Setup: func() {
399+
mockDB.EXPECT().
400+
GetFlagByKey(gomock.Any(), appcfg.FeatureOpenGraphSearch).
401+
Return(appcfg.FeatureFlag{Enabled: true}, nil)
380402
mockGraph.EXPECT().
381-
SearchByNameOrObjectID(gomock.Any(), gomock.Any(), gomock.Any()).
403+
SearchByNameOrObjectID(gomock.Any(), true, "some query", queries.SearchTypeFuzzy).
382404
Return(nil, errors.New("graph error"))
383405
},
384406
Test: func(output apitest.Output) {
@@ -387,14 +409,35 @@ func TestResources_GetSearchResult(t *testing.T) {
387409
},
388410
},
389411
{
390-
Name: "Success",
412+
Name: "Success -- include OpenGraph results",
413+
Input: func(input *apitest.Input) {
414+
apitest.AddQueryParam(input, "query", "some query")
415+
apitest.AddQueryParam(input, "type", "fuzzy")
416+
},
417+
Setup: func() {
418+
mockDB.EXPECT().
419+
GetFlagByKey(gomock.Any(), appcfg.FeatureOpenGraphSearch).
420+
Return(appcfg.FeatureFlag{Enabled: true}, nil)
421+
mockGraph.EXPECT().
422+
SearchByNameOrObjectID(gomock.Any(), true, "some query", queries.SearchTypeFuzzy).
423+
Return(graph.NewNodeSet(), nil)
424+
},
425+
Test: func(output apitest.Output) {
426+
apitest.StatusCode(output, http.StatusOK)
427+
},
428+
},
429+
{
430+
Name: "Success -- exclude OpenGraph results",
391431
Input: func(input *apitest.Input) {
392432
apitest.AddQueryParam(input, "query", "some query")
393433
apitest.AddQueryParam(input, "type", "fuzzy")
394434
},
395435
Setup: func() {
436+
mockDB.EXPECT().
437+
GetFlagByKey(gomock.Any(), appcfg.FeatureOpenGraphSearch).
438+
Return(appcfg.FeatureFlag{Enabled: false}, nil)
396439
mockGraph.EXPECT().
397-
SearchByNameOrObjectID(gomock.Any(), gomock.Any(), gomock.Any()).
440+
SearchByNameOrObjectID(gomock.Any(), false, "some query", queries.SearchTypeFuzzy).
398441
Return(graph.NewNodeSet(), nil)
399442
},
400443
Test: func(output apitest.Output) {

cmd/api/src/api/v2/search.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import (
2323

2424
"github.com/specterops/bloodhound/cmd/api/src/api"
2525
"github.com/specterops/bloodhound/cmd/api/src/model"
26+
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
2627
"github.com/specterops/bloodhound/cmd/api/src/utils"
2728
"github.com/specterops/bloodhound/packages/go/analysis"
2829
"github.com/specterops/bloodhound/packages/go/graphschema"
30+
"github.com/specterops/bloodhound/packages/go/graphschema/ad"
2931
"github.com/specterops/bloodhound/packages/go/graphschema/azure"
3032
"github.com/specterops/bloodhound/packages/go/graphschema/common"
3133
"github.com/specterops/dawgs/graph"
@@ -43,15 +45,26 @@ func (s Resources) SearchHandler(response http.ResponseWriter, request *http.Req
4345
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Invalid search parameter", request), response)
4446
} else if skip, limit, _, err := utils.GetPageParamsForGraphQuery(context.Background(), queryParams); err != nil {
4547
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("Invalid query parameter: %v", err), request), response)
46-
} else if nodeKinds, err := analysis.ParseKinds(nodeTypes...); err != nil {
48+
} else if openGraphSearchFeatureFlag, err := s.DB.GetFlagByKey(request.Context(), appcfg.FeatureOpenGraphSearch); err != nil {
49+
api.HandleDatabaseError(request, response, err)
50+
} else if nodeKinds, err := getNodeKinds(openGraphSearchFeatureFlag.Enabled, nodeTypes...); err != nil {
4751
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Invalid type parameter", request), response)
48-
} else if result, err := s.GraphQuery.SearchNodesByName(ctx, nodeKinds, searchQuery, skip, limit); err != nil {
52+
} else if result, err := s.GraphQuery.SearchNodesByNameOrObjectId(ctx, nodeKinds, searchQuery, skip, limit); err != nil {
4953
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("Graph error: %v", err), request), response)
5054
} else {
5155
api.WriteBasicResponse(request.Context(), result, http.StatusOK, response)
5256
}
5357
}
5458

59+
// getNodeKinds preserves legacy parseKinds behavior when the OpenGraphSearch feature flag is disabled.
60+
func getNodeKinds(openGraphSearchEnabled bool, nodeTypes ...string) (graph.Kinds, error) {
61+
if !openGraphSearchEnabled && len(nodeTypes) == 0 {
62+
return analysis.ParseKinds(ad.Entity.String(), azure.Entity.String())
63+
} else {
64+
return analysis.ParseKinds(nodeTypes...)
65+
}
66+
}
67+
5568
func (s *Resources) GetAvailableDomains(response http.ResponseWriter, request *http.Request) {
5669
var domains model.DomainSelectors
5770

0 commit comments

Comments
 (0)