Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9061c19
Prototyping the idea of compile-time build flags to set DogTags from …
kpom-specter Sep 26, 2025
a140974
BED 6063: changelog HA (#1936)
brandonshearin Sep 26, 2025
8bdc77f
fix(LoginPage) - replace padding with centering in login page (#1941)
dcairnsspecterops Sep 26, 2025
e94d68b
BED-6380: Summary Page Included in BHCE (#1932)
ALCooper12 Sep 26, 2025
7e71a73
chore: BED-6554 Add UTM links to README (#1948)
StephenHinck Sep 28, 2025
8579b25
Add some actual things that are flags
kpom-specter Oct 2, 2025
5685164
Replicated integration
kpom-specter Oct 2, 2025
b2ee476
feat: BED-6556 - Remove API version from User Profile page (#1951)
StephenHinck Oct 6, 2025
6440985
fix (api): Saved Query Multi Export is Not Functioning as Expected -…
LawsonWillard Oct 6, 2025
ba3b54c
BED-6344: Migrate "build-main-commit" & "release-prep" workflows from…
kpowderly Oct 6, 2025
c9149f3
BED-6041 etac available domains (#1925)
bsheth711 Oct 6, 2025
9884b6c
chore+ai: St Bernard Avalanche Training (#1944)
superlinkx Oct 7, 2025
5adc074
feat: replaced ACLs with ACEs in Data Quality (#1959)
specter-flq Oct 8, 2025
624492a
Create a very simple hook copying the datapipe pattern
kpom-specter Oct 8, 2025
2ae6bec
BED-6546: URL encode cypher queries
sirisjo Oct 9, 2025
14c01cf
fix the provider/service/raw naming confusion
kpom-specter Oct 9, 2025
eb2e139
Plumb the dogtags service through so the HTTP handler can fetch it
kpom-specter Oct 9, 2025
0345491
BED-5692: Add scroll to simple domain selector (#1964)
specter-flq Oct 10, 2025
c232697
Prototyping the idea of compile-time build flags to set DogTags from …
kpom-specter Sep 26, 2025
23bd00b
Add some actual things that are flags
kpom-specter Oct 2, 2025
8b6c7a3
Replicated integration
kpom-specter Oct 2, 2025
872814e
Create a very simple hook copying the datapipe pattern
kpom-specter Oct 8, 2025
fbd504e
fix the provider/service/raw naming confusion
kpom-specter Oct 9, 2025
910a33d
Plumb the dogtags service through so the HTTP handler can fetch it
kpom-specter Oct 9, 2025
a98d855
Merge branch 'kpom/openfeature' of github.com:SpecterOps/BloodHound i…
kpom-specter Oct 15, 2025
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
21 changes: 21 additions & 0 deletions .github/workflows/reusable.docker-content-trust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,24 @@ jobs:
if: always()
shell: bash
run: rm -rvf ~/.docker/trust/private/

validate-image:
name: Validate Signed Image
needs: sign-image
runs-on: ubuntu-latest
# continue-on-error until validate signed image can be fully validated post-merge
continue-on-error: true
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_SERVER: ${{ inputs.docker_content_trust_server }}
steps:
- uses: docker/login-action@v3
name: Authenticate with DockerHub Registry
with:
registry: docker.io
username: ${{ secrets.dockerhub_account }}
password: ${{ secrets.dockerhub_token }}

- run: |
echo "Validating signature of image..."
docker pull ${{ inputs.dockerhub_image_reference }}
Comment on lines +116 to +118
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 | 🟠 Major

Guard against digest refs and add explicit trust inspect

Notary v1 trust is tag-based; digest references won’t validate signatures as intended. Enforce tag usage and inspect trust metadata explicitly.

Apply this diff to harden the validation step:

-      - run: |
-          echo "Validating signature of image..."
-          docker pull ${{ inputs.dockerhub_image_reference }}
+      - shell: bash
+        run: |
+          set -euo pipefail
+          ref="${{ inputs.dockerhub_image_reference }}"
+          if [[ "$ref" =~ @sha256: ]]; then
+            echo "Digest references bypass Notary v1 trust; pass a tag (e.g., repo:tag)." >&2
+            exit 1
+          fi
+          echo "Validating signature of image..."
+          docker pull "$ref"
+          docker trust inspect --pretty "$ref"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- run: |
echo "Validating signature of image..."
docker pull ${{ inputs.dockerhub_image_reference }}
- shell: bash
run: |
set -euo pipefail
ref="${{ inputs.dockerhub_image_reference }}"
if [[ "$ref" =~ @sha256: ]]; then
echo "Digest references bypass Notary v1 trust; pass a tag (e.g., repo:tag)." >&2
exit 1
fi
echo "Validating signature of image..."
docker pull "$ref"
docker trust inspect --pretty "$ref"
🤖 Prompt for AI Agents
.github/workflows/reusable.docker-content-trust.yml lines 116-118: the workflow
currently pulls any image reference which allows digest (@sha256:...) refs that
Notary v1 cannot verify; update the step to first reject digest references
(detect '@' or 'sha256:' in the input and exit non-zero with a clear message),
then run docker pull on the validated tag, and finally run an explicit trust
inspection (e.g. docker trust inspect --pretty "${{
inputs.dockerhub_image_reference }}" or similar) and fail the step if the trust
inspect returns non-zero or shows no valid signatures so the job enforces tag
usage and validates Notary trust metadata.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Project Level Git Ignore
################################

license.yaml

# Temporary backup files
*.bak

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

BloodHound is a monolithic web application composed of an embedded React frontend with [Sigma.js](https://www.sigmajs.org/) and a [Go](https://go.dev/) based REST API backend. It is deployed with a [Postgresql](https://www.postgresql.org/) application database and a [Neo4j](https://neo4j.com/) graph database, and is fed by the [SharpHound](https://github.com/SpecterOps/SharpHound) and [AzureHound](https://github.com/SpecterOps/AzureHound) data collectors.

BloodHound leverages graph theory to reveal hidden and often unintended relationships across identity and access management systems. Powered by [OpenGraph](https://specterops.io/opengraph/), BloodHound now supports comprehensive analysis beyond Active Directory and Azure environments, enabling users to map complex privilege relationships across [diverse identity platforms](https://bloodhound.specterops.io/opengraph/library). Attackers can utilize BloodHound to rapidly discover sophisticated attack paths otherwise impossible to identify manually, while defenders can proactively identify and mitigate these risks. Both red and blue teams benefit from BloodHound's expanded capabilities, gaining deeper insights into identity and privilege structures across their entire security landscape.
BloodHound leverages graph theory to reveal hidden and often unintended relationships across identity and access management systems. Powered by [OpenGraph](https://specterops.io/opengraph/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF), BloodHound now supports comprehensive analysis beyond Active Directory and Azure environments, enabling users to map complex privilege relationships across [diverse identity platforms](https://bloodhound.specterops.io/opengraph/library). Attackers can utilize BloodHound to rapidly discover sophisticated attack paths otherwise impossible to identify manually, while defenders can proactively identify and mitigate these risks. Both red and blue teams benefit from BloodHound's expanded capabilities, gaining deeper insights into identity and privilege structures across their entire security landscape.

BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/). The original BloodHound was created by [@\_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF). The original BloodHound was created by [@\_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
Comment on lines +11 to +13
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 | 🟠 Major

Remove tracking parameters from public README links.

Embedding campaign- and lead-specific tracking parameters in README links introduces an avoidable privacy/compliance risk for open-source users. Please revert to the plain URLs.

Apply this diff to restore the clean links:

- BloodHound leverages graph theory to reveal hidden and often unintended relationships across identity and access management systems. Powered by [OpenGraph](https://specterops.io/opengraph/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF), BloodHound now supports comprehensive analysis beyond Active Directory and Azure environments, enabling users to map complex privilege relationships across [diverse identity platforms](https://bloodhound.specterops.io/opengraph/library). Attackers can utilize BloodHound to rapidly discover sophisticated attack paths otherwise impossible to identify manually, while defenders can proactively identify and mitigate these risks. Both red and blue teams benefit from BloodHound's expanded capabilities, gaining deeper insights into identity and privilege structures across their entire security landscape.
-
-BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF). The original BloodHound was created by [@\_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
+BloodHound leverages graph theory to reveal hidden and often unintended relationships across identity and access management systems. Powered by [OpenGraph](https://specterops.io/opengraph/), BloodHound now supports comprehensive analysis beyond Active Directory and Azure environments, enabling users to map complex privilege relationships across [diverse identity platforms](https://bloodhound.specterops.io/opengraph/library). Attackers can utilize BloodHound to rapidly discover sophisticated attack paths otherwise impossible to identify manually, while defenders can proactively identify and mitigate these risks. Both red and blue teams benefit from BloodHound's expanded capabilities, gaining deeper insights into identity and privilege structures across their entire security landscape.
+
+BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/). The original BloodHound was created by [@\_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BloodHound leverages graph theory to reveal hidden and often unintended relationships across identity and access management systems. Powered by [OpenGraph](https://specterops.io/opengraph/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF), BloodHound now supports comprehensive analysis beyond Active Directory and Azure environments, enabling users to map complex privilege relationships across [diverse identity platforms](https://bloodhound.specterops.io/opengraph/library). Attackers can utilize BloodHound to rapidly discover sophisticated attack paths otherwise impossible to identify manually, while defenders can proactively identify and mitigate these risks. Both red and blue teams benefit from BloodHound's expanded capabilities, gaining deeper insights into identity and privilege structures across their entire security landscape.
BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/). The original BloodHound was created by [@\_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/?utm_campaign=Direct_DemoRequest_2025_09_01_GitHub&utm_medium=DemoRequest&utm_source=Direct&Latest_Campaign=701Uw00000X36PF). The original BloodHound was created by [@\_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
BloodHound leverages graph theory to reveal hidden and often unintended relationships across identity and access management systems. Powered by [OpenGraph](https://specterops.io/opengraph/), BloodHound now supports comprehensive analysis beyond Active Directory and Azure environments, enabling users to map complex privilege relationships across [diverse identity platforms](https://bloodhound.specterops.io/opengraph/library). Attackers can utilize BloodHound to rapidly discover sophisticated attack paths otherwise impossible to identify manually, while defenders can proactively identify and mitigate these risks. Both red and blue teams benefit from BloodHound's expanded capabilities, gaining deeper insights into identity and privilege structures across their entire security landscape.
BloodHound CE is created and maintained by the [SpecterOps](https://specterops.io/) team who also brought you [BloodHound Enterprise](https://specterops.io/bloodhound-overview/). The original BloodHound was created by [@_wald0](https://www.twitter.com/_wald0), [@CptJesus](https://twitter.com/CptJesus), and [@harmj0y](https://twitter.com/harmj0y).
🧰 Tools
🪛 LanguageTool

[style] ~11-~11: Consider a different adjective to strengthen your wording.
Context: ...dHound's expanded capabilities, gaining deeper insights into identity and privilege st...

(DEEP_PROFOUND)

🤖 Prompt for AI Agents
In README.md around lines 11 to 13, several public links include tracking/query
parameters (utm_campaign, utm_medium, utm_source, Latest_Campaign) — remove
these query parameters and replace each tracked URL with its clean base URL
(e.g., https://specterops.io/opengraph,
https://bloodhound.specterops.io/opengraph/library, https://specterops.io/,
https://specterops.io/bloodhound-overview and the Twitter profile URLs) so the
README only contains plain, non-tracking links.


## Running BloodHound Community Edition
Please refer to the [Quickstart Guide for BloodHound Community Edition](https://bloodhound.specterops.io/get-started/quickstart/community-edition-quickstart), which is part of the [BloodHound documentation](https://bloodhound.specterops.io).
Expand Down
3 changes: 2 additions & 1 deletion cmd/api/src/api/registration/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func RegisterFossRoutes(
authenticator api.Authenticator,
authorizer auth.Authorizer,
ingestSchema upload.IngestSchema,
dogtagsService v2.DogtagsService,
) {
router.With(func() mux.MiddlewareFunc {
return middleware.DefaultRateLimitMiddleware(rdms)
Expand All @@ -80,6 +81,6 @@ func RegisterFossRoutes(
routerInst.PathPrefix("/ui", static.AssetHandler),
)

var resources = v2.NewResources(rdms, graphDB, cfg, apiCache, graphQuery, collectorManifests, authorizer, authenticator, ingestSchema)
var resources = v2.NewResources(rdms, graphDB, cfg, apiCache, graphQuery, collectorManifests, authorizer, authenticator, ingestSchema, dogtagsService)
NewV2API(resources, routerInst)
}
6 changes: 4 additions & 2 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) {
routerInst.GET("/api/v2/features", resources.GetFlags).RequirePermissions(permissions.AppReadApplicationConfiguration),
routerInst.PUT("/api/v2/features/{feature_id}/toggle", resources.ToggleFlag).RequirePermissions(permissions.AppWriteApplicationConfiguration),

routerInst.GET("/api/v2/dogtags", resources.GetDogtags),

Comment on lines +159 to +160
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 26, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard /api/v2/dogtags behind the appropriate permission

Right now the new route is wide open—no .RequireAuth() and no permission check—so anyone can read whatever the DogTags service returns. That’s a data leak of internal configuration and should be treated as a security defect. Please gate it the same way we already protect /api/v2/features (at minimum AppReadApplicationConfiguration, or an equivalent auth requirement).

-		routerInst.GET("/api/v2/dogtags", resources.GetDogtags),
+		routerInst.GET("/api/v2/dogtags", resources.GetDogtags).RequirePermissions(permissions.AppReadApplicationConfiguration),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
routerInst.GET("/api/v2/dogtags", resources.GetDogtags),
routerInst.GET("/api/v2/dogtags", resources.GetDogtags).RequirePermissions(permissions.AppReadApplicationConfiguration),
🤖 Prompt for AI Agents
In cmd/api/src/api/registration/v2.go around lines 159-160 the router
registration for GET "/api/v2/dogtags" is missing authentication and permission
checks; update the route to require authentication and the same permission used
for /api/v2/features (e.g., AppReadApplicationConfiguration) by chaining the
authentication middleware and permission requirement so the handler is only
reachable by authorized callers—mirror the protection pattern used for
/api/v2/features (add .RequireAuth() and the equivalent .RequirePermission(...)
call or middleware wrapper).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is startup level application settings. What options we even show the user to login, or if the application starts up at all should be listed here. I'm not sure that these should be private? Human talk needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kpom-specter, you raise a valid point about public vs. private information. Let me outline the considerations for your team discussion:

Arguments for keeping it public (no auth):

  • Startup/health information is commonly public in many applications
  • Login method availability needs to be shown to unauthenticated users
  • Allows monitoring systems to check app status without credentials
  • Similar to health check endpoints that are typically open

Arguments for requiring auth:

  • Flags like max_connections and api_base_url could reveal internal architecture details
  • Even "can_app_startup" could give attackers information about system state
  • Information disclosure is often considered a security issue
  • Consistency with other config endpoints like /api/v2/features

Possible middle-ground approach:
Consider splitting the endpoint or flag visibility:

  1. Truly public flags (login methods, basic health) → public endpoint
  2. Internal/operational flags (connection limits, URLs, etc.) → authenticated endpoint

OR

  1. Return a filtered subset of flags for unauthenticated requests, full set for authenticated users

Recommendation:
I'd suggest starting with requiring at least .RequireAuth() (authentication without specific permissions) as a baseline, then your team can decide if specific flags should be publicly exposed through a separate endpoint. This follows the principle of "secure by default, open by exception."

The decision ultimately depends on your security model and whether unauthenticated users genuinely need any of this information to use the application.

routerInst.POST("/api/v2/clear-database", resources.HandleDatabaseWipe).RequirePermissions(permissions.WipeDB),

// Asset Groups API
Expand Down Expand Up @@ -215,15 +217,15 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) {
routerInst.POST("/api/v2/graphs/cypher", resources.CypherQuery).RequirePermissions(permissions.GraphDBRead),
routerInst.GET("/api/v2/saved-queries", resources.ListSavedQueries).RequirePermissions(permissions.SavedQueriesRead),
routerInst.POST("/api/v2/saved-queries", resources.CreateSavedQuery).RequirePermissions(permissions.SavedQueriesWrite),
routerInst.GET("/api/v2/saved-queries/export", resources.ExportSavedQueries).RequirePermissions(permissions.SavedQueriesRead),
routerInst.POST("/api/v2/saved-queries/import", resources.ImportSavedQueries).RequirePermissions(permissions.SavedQueriesWrite),
routerInst.GET(fmt.Sprintf("/api/v2/saved-queries/{%s}", api.URIPathVariableSavedQueryID), resources.GetSavedQuery).RequirePermissions(permissions.SavedQueriesRead),
routerInst.PUT(fmt.Sprintf("/api/v2/saved-queries/{%s}", api.URIPathVariableSavedQueryID), resources.UpdateSavedQuery).RequirePermissions(permissions.SavedQueriesWrite),
routerInst.DELETE(fmt.Sprintf("/api/v2/saved-queries/{%s}", api.URIPathVariableSavedQueryID), resources.DeleteSavedQuery).RequirePermissions(permissions.SavedQueriesWrite),
routerInst.GET(fmt.Sprintf("/api/v2/saved-queries/{%s}/permissions", api.URIPathVariableSavedQueryID), resources.GetSavedQueryPermissions).RequirePermissions(permissions.SavedQueriesRead),
routerInst.DELETE(fmt.Sprintf("/api/v2/saved-queries/{%s}/permissions", api.URIPathVariableSavedQueryID), resources.DeleteSavedQueryPermissions).RequirePermissions(permissions.SavedQueriesWrite),
routerInst.PUT(fmt.Sprintf("/api/v2/saved-queries/{%s}/permissions", api.URIPathVariableSavedQueryID), resources.ShareSavedQueries).RequirePermissions(permissions.SavedQueriesWrite),
routerInst.GET(fmt.Sprintf("/api/v2/saved-queries/{%s}/export", api.URIPathVariableSavedQueryID), resources.ExportSavedQuery).RequirePermissions(permissions.SavedQueriesRead),
routerInst.GET("/api/v2/saved-queries/export", resources.ExportSavedQueries).RequirePermissions(permissions.SavedQueriesRead),
routerInst.POST("/api/v2/saved-queries/import", resources.ImportSavedQueries).RequirePermissions(permissions.SavedQueriesWrite),

// Azure Entity API
routerInst.GET("/api/v2/azure/{entity_type}", resources.GetAZEntity).RequirePermissions(permissions.GraphDBRead),
Expand Down
12 changes: 12 additions & 0 deletions cmd/api/src/api/v2/access_control_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,15 @@ func CheckUserAccessToEnvironments(ctx context.Context, db database.EnvironmentA

return true, nil
}

// ExtractEnvironmentIDsFromUser is a helper function
// to extract a user's environments from their model as a list of strings
func ExtractEnvironmentIDsFromUser(user *model.User) []string {
list := make([]string, 0, len(user.EnvironmentAccessControl))

for _, envAccess := range user.EnvironmentAccessControl {
list = append(list, envAccess.EnvironmentID)
}

return list
}
Comment on lines +63 to +71
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 | 🔴 Critical

Guard against nil pointer dereference.

If user is nil, accessing user.EnvironmentAccessControl on Line 66 will panic.

Apply this diff to add a nil check:

 func ExtractEnvironmentIDsFromUser(user *model.User) []string {
+	if user == nil {
+		return []string{}
+	}
 	list := make([]string, 0, len(user.EnvironmentAccessControl))
 
 	for _, envAccess := range user.EnvironmentAccessControl {
 		list = append(list, envAccess.EnvironmentID)
 	}
 
 	return list
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func ExtractEnvironmentIDsFromUser(user *model.User) []string {
list := make([]string, 0, len(user.EnvironmentAccessControl))
for _, envAccess := range user.EnvironmentAccessControl {
list = append(list, envAccess.EnvironmentID)
}
return list
}
func ExtractEnvironmentIDsFromUser(user *model.User) []string {
if user == nil {
return []string{}
}
list := make([]string, 0, len(user.EnvironmentAccessControl))
for _, envAccess := range user.EnvironmentAccessControl {
list = append(list, envAccess.EnvironmentID)
}
return list
}
🤖 Prompt for AI Agents
In cmd/api/src/api/v2/access_control_list.go around lines 63 to 71, the function
ExtractEnvironmentIDsFromUser can panic when user is nil (and defensively if
user.EnvironmentAccessControl is nil); add a guard at the top that returns an
empty []string if user == nil (and optionally if user.EnvironmentAccessControl
== nil) before trying to access its fields, then proceed to allocate the slice
and iterate as before.

33 changes: 33 additions & 0 deletions cmd/api/src/api/v2/dogtags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package v2

import (
"net/http"

"github.com/specterops/bloodhound/cmd/api/src/api"
)

type DogtagsResponse struct {
Data map[string]interface{} `json:"data"`
}

type DogtagEvaluation struct {
Value interface{} `json:"value"`
Reason string `json:"reason,omitempty"`
Variant string `json:"variant,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
}

func (s Resources) GetDogtags(response http.ResponseWriter, request *http.Request) {
if s.DogtagsService == nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(
http.StatusServiceUnavailable,
"Dogtags service not available",
request,
), response)
return
}

flags := s.DogtagsService.GetAllFlags(request.Context())

api.WriteBasicResponse(request.Context(), flags, http.StatusOK, response)
}
100 changes: 100 additions & 0 deletions cmd/api/src/api/v2/dogtags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package v2_test

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2"
"github.com/specterops/bloodhound/cmd/api/src/services/dogtags"
)

// Mock DogtagsService for testing
type mockDogtagsService struct {
flags map[dogtags.FlagKey]interface{}
}

func (m *mockDogtagsService) GetAllFlags(ctx context.Context) map[dogtags.FlagKey]interface{} {
return m.flags
}


func TestGetDogtags(t *testing.T) {
// Setup mock provider
mockProvider := &mockDogtagsService{
flags: map[dogtags.FlagKey]interface{}{
dogtags.BypassCypherQueryLimits: true,
dogtags.CypherMutability: false,
dogtags.ZoneAllocation: int64(100),
dogtags.LabelAllocation: int64(50),
},
}

// Create resources with service using mock provider
resources := v2.Resources{
DogtagsService: mockProvider,
}

// Create test request
req := httptest.NewRequest("GET", "/api/v2/dogtags", nil)
w := httptest.NewRecorder()

// Call the handler
resources.GetDogtags(w, req)

// Check response
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}

// Parse response body - WriteBasicResponse wraps the flags
var rawResponse map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&rawResponse); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}

// Extract the actual flags data
flagsData, ok := rawResponse["data"].(map[string]interface{})
if !ok {
t.Fatalf("Expected 'data' field with flags, got: %+v", rawResponse)
}

// Verify response data - JSON unmarshaling converts numbers to float64
expectedFlags := map[string]interface{}{
"bypass_cypher_query_limits": true,
"cypher_mutability": false,
"zone_allocation": float64(100),
"label_allocation": float64(50),
}

for key, expectedValue := range expectedFlags {
if actualValue, exists := flagsData[key]; !exists {
t.Errorf("Expected flag %s to exist in response", key)
} else if actualValue != expectedValue {
t.Errorf("Expected flag %s to be %v, got %v", key, expectedValue, actualValue)
}
}
}

func TestGetDogtagsServiceNotAvailable(t *testing.T) {
// Create resources without dogtags service (nil)
resources := v2.Resources{
DogtagsService: nil,
}

// Create test request
req := httptest.NewRequest("GET", "/api/v2/dogtags", nil)
w := httptest.NewRecorder()

// Call the handler
resources.GetDogtags(w, req)

// Check response
resp := w.Result()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Errorf("Expected status 503, got %d", resp.StatusCode)
}
}
7 changes: 7 additions & 0 deletions cmd/api/src/api/v2/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/specterops/bloodhound/cmd/api/src/model"
"github.com/specterops/bloodhound/cmd/api/src/queries"
"github.com/specterops/bloodhound/cmd/api/src/serde"
"github.com/specterops/bloodhound/cmd/api/src/services/dogtags"
"github.com/specterops/bloodhound/cmd/api/src/services/fs"
"github.com/specterops/bloodhound/cmd/api/src/services/upload"
"github.com/specterops/bloodhound/packages/go/cache"
Expand Down Expand Up @@ -101,6 +102,9 @@ type CreateOIDCProviderRequest struct {
ClientId string `json:"client_id"`
}

// DogtagsService alias for the interface from dogtags package
type DogtagsService = dogtags.Service

// Resources holds the database and configuration dependencies to be passed around the API functions
type Resources struct {
Decoder *schema.Decoder
Expand All @@ -115,6 +119,7 @@ type Resources struct {
Authenticator api.Authenticator
IngestSchema upload.IngestSchema
FileService fs.Service
DogtagsService DogtagsService
}

func NewResources(
Expand All @@ -127,6 +132,7 @@ func NewResources(
authorizer auth.Authorizer,
authenticator api.Authenticator,
ingestSchema upload.IngestSchema,
dogtagsService DogtagsService,
) Resources {
return Resources{
Decoder: schema.NewDecoder(),
Expand All @@ -141,5 +147,6 @@ func NewResources(
Authenticator: authenticator,
IngestSchema: ingestSchema,
FileService: &fs.Client{},
DogtagsService: dogtagsService,
}
}
1 change: 1 addition & 0 deletions cmd/api/src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ type Configuration struct {
RecreateDefaultAdmin bool `json:"recreate_default_admin"`
EnableUserAnalytics bool `json:"enable_user_analytics"`
ForceDownloadEmbeddedCollectors bool `json:"force_download_embedded_collectors"`
DogTagsFilePath string `json:"dogtags_file_path"`
}

func (s Configuration) TempDirectory() string {
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func NewDefaultConfiguration() (Configuration, error) {
ExpireNow: true,
},
EnableUserAnalytics: false,
DogTagsFilePath: "", // Empty uses provider defaults
}, nil
}
}
16 changes: 15 additions & 1 deletion cmd/api/src/daemons/changelog/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"log/slog"
"time"

"github.com/specterops/bloodhound/cmd/api/src/daemons/ha"
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
"github.com/specterops/dawgs/graph"
)
Expand Down Expand Up @@ -48,7 +49,20 @@ func DefaultOptions() Options {
}

func NewChangelog(dawgsDB graph.Database, flagProvider appcfg.GetFlagByKeyer, opts Options) *Changelog {
flagManager := newFeatureFlagManager(flagGetter(dawgsDB, flagProvider), opts.PollInterval)
// Use dummy HA implementation for BHCE (always primary)
flagManager := newFeatureFlagManager(flagGetter(dawgsDB, flagProvider), opts.PollInterval, ha.NewDummyHA())
coordinator := newIngestionCoordinator(dawgsDB)

return &Changelog{
flagManager: flagManager,
coordinator: coordinator,
options: opts,
}
}

// NewChangelogWithHA creates a changelog with a real HA implementation for high-availability deployments.
func NewChangelogWithHA(dawgsDB graph.Database, flagProvider appcfg.GetFlagByKeyer, opts Options, haMutex ha.HAMutex) *Changelog {
flagManager := newFeatureFlagManager(flagGetter(dawgsDB, flagProvider), opts.PollInterval, haMutex)
coordinator := newIngestionCoordinator(dawgsDB)

return &Changelog{
Expand Down
Loading