-
Notifications
You must be signed in to change notification settings - Fork 244
[POC] DogTags #1947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[POC] DogTags #1947
Changes from all commits
9061c19
a140974
8bdc77f
e94d68b
7e71a73
8579b25
5685164
b2ee476
6440985
ba3b54c
c9149f3
9884b6c
5adc074
624492a
2ae6bec
14c01cf
eb2e139
0345491
c232697
23bd00b
8b6c7a3
872814e
fbd504e
910a33d
a98d855
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🧰 Tools🪛 LanguageTool[style] ~11-~11: Consider a different adjective to strengthen your wording. (DEEP_PROFOUND) 🤖 Prompt for AI Agents
|
||||||||||||||||
|
||||||||||||||||
## 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). | ||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard Right now the new route is wide open—no - routerInst.GET("/api/v2/dogtags", resources.GetDogtags),
+ routerInst.GET("/api/v2/dogtags", resources.GetDogtags).RequirePermissions(permissions.AppReadApplicationConfiguration), 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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):
Arguments for requiring auth:
Possible middle-ground approach:
OR
Recommendation: 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 | ||||||
|
@@ -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), | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against nil pointer dereference. If 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
Suggested change
🤖 Prompt for AI Agents
|
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) | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
📝 Committable suggestion
🤖 Prompt for AI Agents