Skip to content

Conversation

kpom-specter
Copy link
Contributor

@kpom-specter kpom-specter commented Sep 26, 2025

Prototyping the idea of compile-time build flags to set DogTags from different sources. Made up some fake TagValues to prove datatypes

Description

Describe your changes in detail

Motivation and Context

Resolves <TICKET_OR_ISSUE_NUMBER>

Why is this change required? What problem does it solve?

How Has This Been Tested?

Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc.

Screenshots (optional):

Types of changes

  • Chore (a change that does not modify the application functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Database Migrations

Checklist:

image

Summary by CodeRabbit

  • New Features
    • v2 API endpoint to fetch Dogtags (runtime flags).
    • Saved Queries endpoints for export (GET) and import (POST).
  • Configuration
    • New dogtags_file_path setting to load flags from a YAML file (defaults when empty).
  • Operational Changes
    • Application logs all loaded Dogtags at startup for visibility.
  • Tests
    • Handler tests for successful dogtags retrieval and service-unavailable case.
  • Chores
    • .gitignore updated to exclude license.yaml.

Copy link
Contributor

coderabbitai bot commented Sep 26, 2025

Walkthrough

Adds a Dogtags feature (YAML-backed provider, service, config, startup wiring, API v2 GET /api/v2/dogtags and tests), integrates a DogtagsService into route registration, implements HA abstractions and changelog HA changes, and applies a large cmdrunner ExecutionPlan refactor across many Go packages plus assorted UI and test adjustments.

Changes

Cohort / File(s) Summary of changes
API Registration & Routing
cmd/api/src/api/registration/v2.go, cmd/api/src/api/registration/registration.go
Registers GET /api/v2/dogtagsResources.GetDogtags; RegisterFossRoutes signature extended to accept and forward a v2.DogtagsService.
API v2 Dogtags Handler & Model
cmd/api/src/api/v2/dogtags.go, cmd/api/src/api/v2/dogtags_test.go, cmd/api/src/api/v2/model.go
New handler Resources.GetDogtags, response types (DogtagsResponse, DogtagEvaluation), tests for success and service-unavailable, and Resources gains DogtagsService alias/field and NewResources now accepts the service.
Dogtags Service Core
cmd/api/src/services/dogtags/dogtags.go, cmd/api/src/services/dogtags/providers/yaml.go
New dogtags package: FlagKey/FlagSpec/ValidFlags, Provider interface, service impl with GetAllFlags, NewService, and a YAML provider (YAMLProvider, NewYAMLProvider, typed getters, loadFlags with RWMutex).
Config & Startup Wiring
cmd/api/src/config/config.go, cmd/api/src/config/default.go, cmd/api/src/services/entrypoint.go
Adds DogTagsFilePath configuration, initialize defaults, load YAML provider at startup, construct Dogtags service, log flags on startup, abort on provider load error, and pass service into route registration.
HA Abstraction & Changelog HA
cmd/api/src/daemons/ha/ha.go, cmd/api/src/daemons/changelog/*
New HAMutex & LockResult, NewDummyHA, HA-aware changelog constructors/featureFlagManager changes, polling and cache actions gated by primary lock; associated tests updated/added.
cmdrunner ExecutionPlan Refactor
packages/go/stbernard/cmdrunner/cmdrunner.go, and many call sites (workspace, analyzers, git, yarn, go tooling, tester, etc.)
Replaces granular Run(command, args, path, env) calls with Run(ctx, ExecutionPlan) API, adds ExecutionPlan and ErrCmdExecutionFailed, updates numerous call sites to build plans and call cmdrunner.Run(context.TODO(), plan).
UI & Tests (various)
cmd/ui/src/components/LoginPage.tsx, packages/javascript/bh-shared-ui/src/...
Multiple frontend edits: layout change on LoginPage, various PrivilegeZones refactors (DefaultRoot, Summary removal, context/defaultPath changes), icon export adjustments (DownArrow/LargeRightArrow), selector additions, tests updated/removed, small text/label tweaks.
Miscellaneous
.gitignore, README.md, other small edits
Adds license.yaml to .gitignore; README link UTM updates and small low-impact changes (e.g., freeIconsList header, minor test mock changes).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Router as API Router (v2)
  participant Res as Resources
  participant Svc as DogtagsService

  Client->>Router: GET /api/v2/dogtags
  Router->>Res: GetDogtags(w, r)
  alt DogtagsService not configured
    Res-->>Client: 503 Service Unavailable
  else Service available
    Res->>Svc: GetAllFlags(ctx)
    Svc-->>Res: map[FlagKey]interface{}
    Res-->>Client: 200 OK (JSON { data: {...} })
  end
  note over Router,Res: Route registered without RequirePermissions guard
Loading
sequenceDiagram
  autonumber
  participant Entrypoint as Entrypoint
  participant Config as Configuration
  participant Prov as YAMLProvider
  participant Svc as Dogtags Service
  participant RouterReg as Route Registration

  Entrypoint->>Config: read DogTagsFilePath
  Entrypoint->>Prov: NewYAMLProvider(path)
  alt Provider load error
    Prov-->>Entrypoint: error
    Entrypoint->>Entrypoint: log.Fatal and abort
  else Loaded
    Prov-->>Entrypoint: provider
    Entrypoint->>Svc: NewService(provider)
    Entrypoint->>Svc: GetAllFlags(ctx)
    Svc-->>Entrypoint: flags map
    Entrypoint->>Entrypoint: log flags
    Entrypoint->>RouterReg: RegisterFossRoutes(..., dogtagsService)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement, api

Suggested reviewers

  • brandonshearin
  • maffkipp

Poem

I nibble on YAML by moonlit light,
flags lined up like carrots bright.
Startup hums, the router wakes,
GET returns the tiny flakes.
Hop, log, and keep the garden right. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description is largely unfilled placeholder text from the template and the issue placeholder has not been replaced, so it lacks any detailed Description, Motivation, Testing steps, or completed checklist items. Fill in each template section with substantive content: provide a clear description of your changes, link to the actual issue number, explain why the change is needed, detail how you tested it, and update the checklist to reflect completed actions.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.47% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “[POC] DogTags” succinctly identifies the primary change—adding DogTags functionality—making it clear and specific to the main feature introduced by the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kpom/openfeature

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/api/src/api/v2/model.go (1)

105-149: Wire DogtagsService into NewResources

The new DogtagsService field never gets populated when we build the API resources; NewResources still returns the struct with this field left nil. In production that means GetDogtags will always bail out with a 503. Please extend the constructor (and its callers) to accept and assign the service so the endpoint can succeed.

-func NewResources(
+func NewResources(
 	rdms database.Database,
 	graphDB *graph.DatabaseSwitch,
 	cfg config.Configuration,
 	apiCache cache.Cache,
 	graphQuery queries.Graph,
 	collectorManifests config.CollectorManifests,
 	authorizer auth.Authorizer,
 	authenticator api.Authenticator,
 	ingestSchema upload.IngestSchema,
+	dogtagsService dogtags.Provider,
 ) Resources {
 	return Resources{
 		Decoder:                    schema.NewDecoder(),
 		DB:                         rdms,
 		Graph:                      graphDB, // TODO: to be phased out in favor of graph queries
 		GraphQuery:                 graphQuery,
 		Config:                     cfg,
 		QueryParameterFilterParser: model.NewQueryParameterFilterParser(),
 		Cache:                      apiCache,
 		CollectorManifests:         collectorManifests,
 		Authorizer:                 authorizer,
 		Authenticator:              authenticator,
 		IngestSchema:               ingestSchema,
 		FileService:                &fs.Client{},
+		DogtagsService:             dogtagsService,
 	}
 }
🧹 Nitpick comments (5)
cmd/api/src/services/dogtags/dogtags.go (1)

69-79: Surface unsupported flag types.

If someone adds a new entry to ValidFlags with a typo or genuinely new Type, the loop quietly omits that flag and the API response silently regresses. Tighten this by handling the default branch—either return an error (which means widening GetAllFlags/Provider to return (map[FlagKey]interface{}, error)) or at least emit a hard failure so we catch it in tests instead of production.

cmd/api/src/api/v2/dogtags_test.go (2)

47-56: Close the recorder response body in the tests.

httptest.ResponseRecorder.Result() returns a response whose body should be closed; otherwise, we leak the underlying reader if the test grows more complex. Please add a defer resp.Body.Close() immediately after retrieving the result.

Here is a concrete change you can apply:

-	resp := w.Result()
+	resp := w.Result()
+	defer resp.Body.Close()

94-97: Wrap the error-path assertion with body cleanup.

Same as the success path, we should close the body returned by w.Result() to avoid leaking the reader even in tests.

Perform the same adjustment here:

-	resp := w.Result()
+	resp := w.Result()
+	defer resp.Body.Close()
cmd/api/src/api/v2/dogtags.go (1)

9-18: Drop the unused response structs.

DogtagsResponse and DogtagEvaluation are exported but unused, which adds noise and invites confusion about the intended payload shape. Please remove them (or wire them in) once the handler actually serializes that structure.

cmd/api/src/services/dogtags/providers/yaml.go (1)

76-86: Handle integer YAML values in GetFloatFlag.

YAML often emits whole numbers as int even when the schema expects a float. Right now such entries fall through to the default, which is surprising. Convert int/int64 to float64 before giving up.

You can extend the switch like this:

-	if value, exists := p.flags[key]; exists {
-		if floatVal, ok := value.(float64); ok {
-			return floatVal
-		}
-	}
+	if value, exists := p.flags[key]; exists {
+		switch v := value.(type) {
+		case float64:
+			return v
+		case int:
+			return float64(v)
+		case int64:
+			return float64(v)
+		}
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 114f763 and 9061c19.

📒 Files selected for processing (8)
  • cmd/api/src/api/registration/v2.go (1 hunks)
  • cmd/api/src/api/v2/dogtags.go (1 hunks)
  • cmd/api/src/api/v2/dogtags_test.go (1 hunks)
  • cmd/api/src/api/v2/model.go (3 hunks)
  • cmd/api/src/services/dogtags/dogtags.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/noop.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/yaml.go (1 hunks)
  • prove_switching.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-22T20:30:34.839Z
Learnt from: LawsonWillard
PR: SpecterOps/BloodHound#1700
File: cmd/api/src/api/v2/saved_queries_test.go:3182-3182
Timestamp: 2025-07-22T20:30:34.839Z
Learning: In Go table-driven tests in cmd/api/src/api/v2/saved_queries_test.go, subtest parallelization with t.Parallel() is acceptable when tests are self-contained, each creating their own mock controller (gomock.NewController(t)) and having isolated mock expectations without shared state between subtests.

Applied to files:

  • cmd/api/src/api/v2/dogtags_test.go
🧬 Code graph analysis (7)
cmd/api/src/api/v2/model.go (1)
cmd/api/src/services/dogtags/dogtags.go (1)
  • Provider (48-50)
prove_switching.go (1)
cmd/api/src/services/dogtags/dogtags.go (3)
  • Config (11-13)
  • NewService (86-92)
  • FlagKey (16-16)
cmd/api/src/api/v2/dogtags_test.go (2)
cmd/api/src/services/dogtags/dogtags.go (4)
  • FlagKey (16-16)
  • CanAppStartup (20-20)
  • MaxConnections (21-21)
  • ApiBaseURL (22-22)
cmd/api/src/api/v2/model.go (2)
  • Resources (109-123)
  • DogtagsService (106-106)
cmd/api/src/services/dogtags/providers/noop.go (1)
cmd/api/src/services/dogtags/providers/yaml.go (1)
  • NewProvider (20-35)
cmd/api/src/services/dogtags/providers/yaml.go (1)
cmd/api/src/services/dogtags/providers/noop.go (1)
  • NewProvider (11-13)
cmd/api/src/services/dogtags/dogtags.go (2)
cmd/api/src/services/dogtags/providers/noop.go (1)
  • NewProvider (11-13)
cmd/api/src/services/dogtags/providers/yaml.go (1)
  • NewProvider (20-35)
cmd/api/src/api/v2/dogtags.go (3)
cmd/api/src/api/v2/model.go (2)
  • Resources (109-123)
  • DogtagsService (106-106)
cmd/api/src/api/marshalling.go (2)
  • WriteErrorResponse (77-85)
  • WriteBasicResponse (90-99)
cmd/api/src/api/error.go (1)
  • BuildErrorResponse (131-142)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-analysis
  • GitHub Check: run-tests
  • GitHub Check: run-tests
  • GitHub Check: build-ui

Comment on lines +159 to +160
routerInst.GET("/api/v2/dogtags", resources.GetDogtags),

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.

Comment on lines 11 to 13
func NewProvider(filePath string) (*NoOpProvider, error) {
return &NoOpProvider{}, nil
}
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

Fix unused filePath parameter to keep noop builds compiling.

When the noop build tag is enabled, this function is compiled and filePath becomes an unused parameter, yielding cmd/api/src/services/dogtags/providers/noop.go:12:6: filePath declared but not used. Use the blank identifier so we still satisfy the shared signature without tripping the compiler.

-func NewProvider(filePath string) (*NoOpProvider, error) {
+func NewProvider(_ string) (*NoOpProvider, error) {
 	return &NoOpProvider{}, nil
}
📝 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 NewProvider(filePath string) (*NoOpProvider, error) {
return &NoOpProvider{}, nil
}
func NewProvider(_ string) (*NoOpProvider, error) {
return &NoOpProvider{}, nil
}
🤖 Prompt for AI Agents
In cmd/api/src/services/dogtags/providers/noop.go around lines 11 to 13, the
filePath parameter is unused under the noop build tag causing a compile error;
fix it by explicitly referencing it with the blank identifier (e.g., assign
filePath to _ at the start of the function) so the function keeps the shared
signature but the compiler no longer considers filePath unused.

brandonshearin and others added 5 commits September 26, 2025 11:44
* Bed 6063 ha dummy (#1901)

* initial pass

* ha;

* add cache getter

* move to ha package

* simplify flusher. timer -> ticker

* remove unused exported func
* BED-6159 Verbiage Alignment (#1851)

* BED-6159 change string values/paths of zone-management to privilege-zones

* BED-6159 structure change

* BED-6159 app rendering with new URL changes

* BED-6159 pfc

* BED-6159 more details restructure

* BED-6159 structure change for /save

* BED-6159 update root import

* BED-6159 update variable names to use PZ

* BED-6159 more URL structure updates

* BED-6159 missed renames

* BED-6159 update tierid to zoneid, and string tier to string zone

* BED-6159 back pedal on tierZero / zoneZero

* BED-6159 rename assetGroupTagType, fix typo, add todo comments

* BED-6159 remove comments

* BED-6159 pfc

* BED-6159 more tier to zone, restructure URLs, tierId to zoneId

* BED-6159 update tests, tier to zone

* BED-6159 missed tierIds

* BED-6159 missed URLs in tests

* BED-6159 pfc and more tier to zone syntax change

* BED-6159 update label and zone location, remove tagId

* BED-6159 pfc and one more sneaky tier to zone change

* BED-6159 missed file for tier to zone rename

* BED-6159 remove topTagId, linting

* BED-6159 update tagKind to tagType

* BED-6159 remove comment, use variables in routes

* BED-6159 more tagKind to tagType

* BED-6159 update route name for clarity

* BED-6159 use route constants in Details test

* BED-6159 rename variables

* BED-6159 update handleCreateNavigate

* BED-6159 pfc

* BED-6159 remove slash from SummaryCard

* BED-6159 coderabbit add addtl check for tagId

* BED-6159 restructure using newer, shorter, constants

* BED-6159 use new variable to distinguish between zoneId and zoneLocation

* BED-6159 address PR feedback

* BED-6159 revert topTagId

* BED-6159 update paths to be plural

* BED-6159 flip zoneId logic in Details

* BED-6159 pfc

* BED-6159 update missed zoneId and use constants in test

* BED-6159 revert agt.go

* BED-6159 more renames

* BED-6159 rename and logic update for edit button

* BED-6159 PR suggestions

* refactor: util consolidation

* BED-6159 selector path rename

* fix: patch unit tests after refactor

* BED-6159 add conditional check for edit button

* BED-6159 zone management root to PZ root

* BED-6159 route rename

* BED-6159 clean up and PR feedback changes

* BED-6159 PR feedback changes, rename all zone mgmt to pz

* BED-6159 PR feedback changes, rename all zone mgmt to pz

* BED-6159 PR feedback fixes

* BED-6159 pfc

---------

Co-authored-by: Ulises Rangel <[email protected]>

* fix(PZ): Summary page included in BHCE

* fix: moved summary components to BHE

- removed duplicate exports
- fix weird bh-shared-ui imports
- updated PZ context to take summary component
- conditionally render summary
- conditionally initialized summary routes

* just prepare-for-codereview

* added missing MSW handlers to stop unnecessary warning and changed routes set up

* Addressed PR feedback

* Addressed PR feedback

---------

Co-authored-by: Leah Miller <[email protected]>
Co-authored-by: Ulises Rangel <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
cmd/api/src/services/dogtags/dogtags.go (5)

26-30: Consider using constants for flag types to prevent typos.

The Type field is a free-form string, which could lead to runtime errors from typos (e.g., "boo1" instead of "bool").

Define type constants:

+// Flag type constants
+const (
+	FlagTypeBool   = "bool"
+	FlagTypeString = "string"
+	FlagTypeInt    = "int"
+	FlagTypeFloat  = "float"
+)
+
 // FlagSpec defines the specification for a dogtag
 type FlagSpec struct {
 	Type        string `json:"type"`
 	Description string `json:"description,omitempty"`
 }

Then update ValidFlags to use these constants:

 var ValidFlags = map[FlagKey]FlagSpec{
-	BypassCypherQueryLimits: {Type: "bool", Description: "Bypass cypher query limits"},
+	BypassCypherQueryLimits: {Type: FlagTypeBool, Description: "Bypass cypher query limits"},
 	// ... etc
 }

32-38: LGTM! ValidFlags provides a clear registry of supported flags.

The hardcoded map prevents runtime addition of arbitrary flags, which is good for security and predictability.

Minor observation: The code supports float type flags (line 79-80), but no flags currently use this type. If float flags are not needed, consider removing the float handling code to reduce complexity.


49-52: Consider adding typed accessor methods for better type safety.

The GetAllFlags method returns map[FlagKey]interface{}, requiring clients to type-assert values. This is error-prone and loses compile-time type safety.

Consider adding typed accessor methods to the Provider interface:

type Provider interface {
	GetAllFlags(ctx context.Context) map[FlagKey]interface{}
	GetBoolFlag(ctx context.Context, key FlagKey) (bool, error)
	GetIntFlag(ctx context.Context, key FlagKey) (int64, error)
	GetStringFlag(ctx context.Context, key FlagKey) (string, error)
	GetFloatFlag(ctx context.Context, key FlagKey) (float64, error)
}

This would allow clients to retrieve flags with compile-time type safety:

enabled, err := provider.GetBoolFlag(ctx, dogtags.BypassCypherQueryLimits)
if err != nil {
	// handle error
}

54-60: Consider error handling in rawProvider interface.

The rawProvider methods return values directly without errors, forcing providers to either:

  1. Always return defaults on failure (hiding errors)
  2. Panic on critical errors (poor error handling)

This design prevents callers from distinguishing between:

  • A flag explicitly set to its default value
  • A flag that failed to load (returning the default as a fallback)

Consider returning errors:

 type rawProvider interface {
-	GetBoolFlag(ctx context.Context, key string, defaultValue bool) bool
+	GetBoolFlag(ctx context.Context, key string, defaultValue bool) (bool, error)
-	GetStringFlag(ctx context.Context, key string, defaultValue string) string
+	GetStringFlag(ctx context.Context, key string, defaultValue string) (string, error)
-	GetIntFlag(ctx context.Context, key string, defaultValue int64) int64
+	GetIntFlag(ctx context.Context, key string, defaultValue int64) (int64, error)
-	GetFloatFlag(ctx context.Context, key string, defaultValue float64) float64
+	GetFloatFlag(ctx context.Context, key string, defaultValue float64) (float64, error)
 }

Then update GetAllFlags to handle errors appropriately (log, return default, or propagate).


62-65: Document rawProvider thread-safety requirement
YAMLProvider uses an RWMutex and NoOpProvider is stateless, so both are safe for concurrent calls. Add a comment on the rawProvider interface stating that all implementations must support concurrent use.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9061c19 and 8579b25.

📒 Files selected for processing (3)
  • cmd/api/src/services/dogtags/dogtags.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/yaml.go (1 hunks)
  • prove_switching.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • prove_switching.go
  • cmd/api/src/services/dogtags/providers/yaml.go
🧰 Additional context used
🧬 Code graph analysis (1)
cmd/api/src/services/dogtags/dogtags.go (2)
cmd/api/src/services/dogtags/providers/yaml.go (1)
  • NewProvider (20-35)
cmd/api/src/services/dogtags/providers/noop.go (1)
  • NewProvider (11-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: build-ui
  • GitHub Check: run-analysis
  • GitHub Check: run-tests
  • GitHub Check: run-tests
🔇 Additional comments (2)
cmd/api/src/services/dogtags/dogtags.go (2)

15-24: LGTM! Good use of typed constants for flag keys.

The FlagKey type alias provides type safety and prevents accidental use of arbitrary strings. The defined constants are clear and descriptive.

Note: BypassCypherQueryLimits is security-sensitive—ensure proper authorization checks are enforced at the point of use.


87-94: LGTM! Factory function correctly delegates to provider.

The NewService function properly:

  • Delegates provider creation to the providers package
  • Handles errors from provider initialization
  • Returns the Provider interface for flexibility

Note: Ensure config.FilePath is validated before calling this function (see earlier comment on lines 10-13).

Comment on lines 10 to 13
// Config for dogtags service
type Config struct {
FilePath string `yaml:"file_path"`
}
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

Add validation for FilePath to prevent path traversal and ensure file accessibility.

The Config struct accepts an arbitrary FilePath with no validation. This could allow:

  • Path traversal attacks (e.g., ../../etc/passwd)
  • Access to sensitive system files
  • Non-existent or inaccessible paths causing runtime errors

Consider adding validation in NewService to:

  • Reject paths containing .. or absolute paths outside allowed directories
  • Verify the file exists and is readable before attempting to load
  • Set reasonable defaults and document expected file locations
 // Config for dogtags service
 type Config struct {
-	FilePath string `yaml:"file_path"`
+	FilePath string `yaml:"file_path"` // Path to dogtags configuration file (relative to working directory)
 }

Example validation in NewService:

import (
	"path/filepath"
	"strings"
)

func NewService(config Config) (Provider, error) {
	// Validate and sanitize file path
	if config.FilePath != "" {
		cleanPath := filepath.Clean(config.FilePath)
		if strings.Contains(cleanPath, "..") {
			return nil, fmt.Errorf("invalid file path: path traversal detected")
		}
		if filepath.IsAbs(cleanPath) {
			return nil, fmt.Errorf("invalid file path: absolute paths not allowed")
		}
	}
	
	rawProvider, err := providers.NewProvider(config.FilePath)
	if err != nil {
		return nil, err
	}
	return &service{provider: rawProvider}, nil
}
🤖 Prompt for AI Agents
In cmd/api/src/services/dogtags/dogtags.go around lines 10 to 13, the
Config.FilePath is accepted without validation; update NewService to validate
and sanitize the path: use filepath.Clean on the provided path, reject paths
containing ".." or absolute paths outside the allowed base (or enforce a
configured base directory), check existence and readability via os.Stat and
ensure it's a regular file and readable, return a clear error when validation
fails, and provide a sensible default or documented expected location when
config.FilePath is empty.

Comment on lines +67 to +85
func (s *service) GetAllFlags(ctx context.Context) map[FlagKey]interface{} {
result := make(map[FlagKey]interface{})

// Loop through all known flags and get their values from the provider
for flagKey, spec := range ValidFlags {
switch spec.Type {
case "bool":
result[flagKey] = s.provider.GetBoolFlag(ctx, string(flagKey), false)
case "string":
result[flagKey] = s.provider.GetStringFlag(ctx, string(flagKey), "")
case "int":
result[flagKey] = s.provider.GetIntFlag(ctx, string(flagKey), 0)
case "float":
result[flagKey] = s.provider.GetFloatFlag(ctx, string(flagKey), 0.0)
}
}

return result
}
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

Add default case to detect invalid flag types and consider semantic defaults.

The switch statement has no default case. If an invalid type is added to ValidFlags (e.g., typo), the flag will be silently skipped without any error or warning.

Additionally, the hardcoded default values (false, "", 0, 0.0) may not be semantically correct for all flags. For example:

  • ZoneAllocation defaulting to 0 might mean "no zones allowed" vs. an intended default like -1 for "unlimited"
  • BypassCypherQueryLimits defaulting to false is likely correct (secure default)
 func (s *service) GetAllFlags(ctx context.Context) map[FlagKey]interface{} {
 	result := make(map[FlagKey]interface{})
 
 	// Loop through all known flags and get their values from the provider
 	for flagKey, spec := range ValidFlags {
 		switch spec.Type {
 		case "bool":
 			result[flagKey] = s.provider.GetBoolFlag(ctx, string(flagKey), false)
 		case "string":
 			result[flagKey] = s.provider.GetStringFlag(ctx, string(flagKey), "")
 		case "int":
 			result[flagKey] = s.provider.GetIntFlag(ctx, string(flagKey), 0)
 		case "float":
 			result[flagKey] = s.provider.GetFloatFlag(ctx, string(flagKey), 0.0)
+		default:
+			// This should never happen if ValidFlags is maintained correctly
+			// Consider logging or returning an error instead
+			panic(fmt.Sprintf("unknown flag type %q for flag %q", spec.Type, flagKey))
 		}
 	}
 
 	return result
 }

For semantic defaults, consider defining them per-flag in FlagSpec:

type FlagSpec struct {
	Type         string      `json:"type"`
	Description  string      `json:"description,omitempty"`
	DefaultValue interface{} `json:"-"` // Not exposed in JSON
}
🤖 Prompt for AI Agents
In cmd/api/src/services/dogtags/dogtags.go around lines 67 to 85, the switch
over spec.Type lacks a default branch so invalid/typo types in ValidFlags are
silently ignored, and the code uses hardcoded literal defaults which may be
semantically wrong for some flags; update FlagSpec to include a DefaultValue
field (interface{}) and use that per-flag default when calling the provider
getters instead of the literal false/""/0/0.0, and add a default case in the
switch that either logs an error (or returns an error/records a sentinel) when
spec.Type is unrecognized so invalid types are detected at runtime.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
cmd/api/src/services/entrypoint.go (1)

101-110: Don’t log full flag payloads; log keys/count only. Also verify service is actually wired into the API.

Sensitive/license data may be exposed in logs. Limit to keys and a count. And ensure dogtagsService is passed into route/resource wiring; it’s currently only used for logging.

Apply this change:

-	// Log loaded flags on startup
-	flags := dogtagsService.GetAllFlags(ctx)
-	slog.InfoContext(ctx, "DogTags loaded", slog.Any("flags", flags))
+	// Log loaded flags on startup (keys and count only to avoid leaking values)
+	flags := dogtagsService.GetAllFlags(ctx)
+	keys := make([]string, 0, len(flags))
+	for k := range flags {
+		keys = append(keys, k)
+	}
+	slog.InfoContext(ctx, "DogTags loaded", slog.Int("count", len(keys)), slog.Any("keys", keys))
cmd/api/src/services/dogtags/providers/yaml.go (1)

76-86: Handle integer-backed values in GetFloatFlag.

YAML numbers may be unmarshaled as int when configured as whole numbers. Return float64 for int/int64 too.

 func (p *YAMLProvider) GetFloatFlag(ctx context.Context, key string, defaultValue float64) float64 {
   p.mu.RLock()
   defer p.mu.RUnlock()

   if value, exists := p.flags[key]; exists {
-    if floatVal, ok := value.(float64); ok {
-      return floatVal
-    }
+    switch v := value.(type) {
+    case float64:
+      return v
+    case int:
+      return float64(v)
+    case int64:
+      return float64(v)
+    }
   }
   return defaultValue
 }
cmd/api/src/services/dogtags/providers/replicated.go (2)

118-128: Handle integer-backed values in GetFloatFlag.

Support int/int64 to avoid defaulting when the YAML value is a whole number.

 func (p *ReplicatedProvider) GetFloatFlag(ctx context.Context, key string, defaultValue float64) float64 {
   p.mu.RLock()
   defer p.mu.RUnlock()

   if value, exists := p.flags[key]; exists {
-    if floatVal, ok := value.(float64); ok {
-      return floatVal
-    }
+    switch v := value.(type) {
+    case float64:
+      return v
+    case int:
+      return float64(v)
+    case int64:
+      return float64(v)
+    }
   }
   return defaultValue
 }

248-263: Abstract MD5 hashing and document spec requirement
Per Replicated docs, MD5 is mandated for RSA-PSS signature verification. Extract the hash algorithm into a strategy interface and add a code comment referencing the Replicated spec.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8579b25 and 5685164.

📒 Files selected for processing (6)
  • .gitignore (1 hunks)
  • cmd/api/src/config/config.go (1 hunks)
  • cmd/api/src/config/default.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/replicated.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/yaml.go (1 hunks)
  • cmd/api/src/services/entrypoint.go (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🧰 Additional context used
🧬 Code graph analysis (3)
cmd/api/src/services/entrypoint.go (1)
cmd/api/src/services/dogtags/dogtags.go (1)
  • NewService (88-94)
cmd/api/src/services/dogtags/providers/replicated.go (1)
cmd/api/src/services/dogtags/providers/yaml.go (1)
  • NewProvider (20-35)
cmd/api/src/services/dogtags/providers/yaml.go (2)
cmd/api/src/services/dogtags/providers/replicated.go (1)
  • NewProvider (60-75)
cmd/api/src/services/dogtags/providers/noop.go (1)
  • NewProvider (11-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-tests
  • GitHub Check: run-analysis
  • GitHub Check: build-ui
  • GitHub Check: run-tests
🔇 Additional comments (3)
cmd/api/src/config/config.go (1)

169-170: DogTagsFilePath schema addition looks good; please update docs and env mapping examples.

Confirm bhe_dogtags_file_path is documented in config samples and README, and any config migration notes are added.

cmd/api/src/config/default.go (1)

88-89: Default value OK; document provider-specific defaults.

Note in docs: empty uses provider defaults (yaml: local-harnesses/dogtags.yaml; replicated: license.yaml), so operators know what happens if unset.

cmd/api/src/services/entrypoint.go (1)

42-43: Import addition LGTM.

Importing dogtags package is appropriate for service initialization.

Comment on lines 130 to 206
func (p *ReplicatedProvider) loadFlags() error {
slog.Info("================================================================================")
slog.Info("|| ||")
slog.Info("|| REPLICATED LICENSE VALIDATION ||")
slog.Info("|| ||")
slog.Info("================================================================================")
slog.Info(fmt.Sprintf("License file path: %s", p.filePath))

// Read license file
slog.Info("Step 1/4: Reading license file from disk...")
data, err := os.ReadFile(p.filePath)
if err != nil {
slog.Error("================================================================================")
slog.Error("|| FATAL: LICENSE FILE NOT FOUND OR UNREADABLE ||")
slog.Error("================================================================================")
panic(fmt.Sprintf("failed to read license file %s: %v", p.filePath, err))
}
slog.Info("✓ License file read successfully")

// Parse YAML
slog.Info("Step 2/4: Parsing license YAML structure...")
var license ReplicatedLicense
if err := yaml.Unmarshal(data, &license); err != nil {
slog.Error("================================================================================")
slog.Error("|| FATAL: LICENSE FILE MALFORMED OR CORRUPTED ||")
slog.Error("================================================================================")
panic(fmt.Sprintf("failed to parse license YAML file %s: %v", p.filePath, err))
}
slog.Info("✓ License YAML parsed successfully")

// Validate license structure
slog.Info("Step 3/4: Validating license metadata...")
if license.Kind != "License" {
slog.Error("================================================================================")
slog.Error("|| FATAL: INVALID LICENSE FILE FORMAT ||")
slog.Error("================================================================================")
panic(fmt.Sprintf("invalid license file: expected kind 'License', got '%s'", license.Kind))
}
slog.Info(fmt.Sprintf(" License ID: %s", license.Spec.LicenseID))
slog.Info(fmt.Sprintf(" App Slug: %s", license.Spec.AppSlug))
slog.Info(fmt.Sprintf(" Channel: %s", license.Spec.ChannelName))
slog.Info(fmt.Sprintf(" Entitlements found: %d", len(license.Spec.Entitlements)))
slog.Info("✓ License metadata valid")

// Parse public key
slog.Info("Step 4/4: Verifying cryptographic signatures...")
pubKey, err := parsePublicKey(replicatedPublicKey)
if err != nil {
slog.Error("================================================================================")
slog.Error("|| FATAL: INTERNAL ERROR - INVALID PUBLIC KEY ||")
slog.Error("================================================================================")
panic(fmt.Sprintf("failed to parse replicated public key: %v", err))
}

// Extract and verify entitlements
flags := make(map[string]interface{})
for key, entitlement := range license.Spec.Entitlements {
slog.Info(fmt.Sprintf(" Verifying entitlement: %s", key))

// Check that signature exists (basic tamper detection)
if entitlement.Signature.V1 == "" {
slog.Error("================================================================================")
slog.Error(fmt.Sprintf("|| FATAL: MISSING SIGNATURE FOR '%s'", key))
slog.Error("|| This indicates license tampering or corruption. ||")
slog.Error("================================================================================")
panic(fmt.Sprintf("license entitlement '%s' is missing signature - possible tampering detected", key))
}

// Verify signature
if err := verifySignature(entitlement.Value, entitlement.Signature.V1, pubKey); err != nil {
slog.Error("================================================================================")
slog.Error(fmt.Sprintf("|| FATAL: SIGNATURE VERIFICATION FAILED FOR '%s'", key))
slog.Error("|| This indicates license tampering or manual modification. ||")
slog.Error("================================================================================")
panic(fmt.Sprintf("license entitlement '%s' signature verification failed: %v", key, err))
}

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

Replace panics with error returns; fail gracefully and avoid excessive fatal logging.

loadFlags panics on file-read, parse, validation, public key parsing, missing signature, and signature verification. This will crash the API process on startup. Return errors instead and let callers decide (entrypoint already handles errors). Also reduce “FATAL” banners; keep concise structured logs.

Apply these changes:

-	data, err := os.ReadFile(p.filePath)
+	data, err := os.ReadFile(p.filePath)
 	if err != nil {
-		slog.Error("================================================================================")
-		slog.Error("||  FATAL: LICENSE FILE NOT FOUND OR UNREADABLE                            ||")
-		slog.Error("================================================================================")
-		panic(fmt.Sprintf("failed to read license file %s: %v", p.filePath, err))
+		return fmt.Errorf("failed to read license file %s: %w", p.filePath, err)
 	}
@@
 	var license ReplicatedLicense
 	if err := yaml.Unmarshal(data, &license); err != nil {
-		slog.Error("================================================================================")
-		slog.Error("||  FATAL: LICENSE FILE MALFORMED OR CORRUPTED                             ||")
-		slog.Error("================================================================================")
-		panic(fmt.Sprintf("failed to parse license YAML file %s: %v", p.filePath, err))
+		return fmt.Errorf("failed to parse license YAML file %s: %w", p.filePath, err)
 	}
@@
 	if license.Kind != "License" {
-		slog.Error("================================================================================")
-		slog.Error("||  FATAL: INVALID LICENSE FILE FORMAT                                     ||")
-		slog.Error("================================================================================")
-		panic(fmt.Sprintf("invalid license file: expected kind 'License', got '%s'", license.Kind))
+		return fmt.Errorf("invalid license file: expected kind 'License', got %q", license.Kind)
 	}
@@
 	pubKey, err := parsePublicKey(replicatedPublicKey)
 	if err != nil {
-		slog.Error("================================================================================")
-		slog.Error("||  FATAL: INTERNAL ERROR - INVALID PUBLIC KEY                             ||")
-		slog.Error("================================================================================")
-		panic(fmt.Sprintf("failed to parse replicated public key: %v", err))
+		return fmt.Errorf("failed to parse replicated public key: %w", err)
 	}
@@
 	for key, entitlement := range license.Spec.Entitlements {
 		slog.Info(fmt.Sprintf("  Verifying entitlement: %s", key))
@@
 		if entitlement.Signature.V1 == "" {
-			slog.Error("================================================================================")
-			slog.Error(fmt.Sprintf("||  FATAL: MISSING SIGNATURE FOR '%s'", key))
-			slog.Error("||  This indicates license tampering or corruption.                        ||")
-			slog.Error("================================================================================")
-			panic(fmt.Sprintf("license entitlement '%s' is missing signature - possible tampering detected", key))
+			return fmt.Errorf("license entitlement %q is missing signature", key)
 		}
@@
 		if err := verifySignature(entitlement.Value, entitlement.Signature.V1, pubKey); err != nil {
-			slog.Error("================================================================================")
-			slog.Error(fmt.Sprintf("||  FATAL: SIGNATURE VERIFICATION FAILED FOR '%s'", key))
-			slog.Error("||  This indicates license tampering or manual modification.               ||")
-			slog.Error("================================================================================")
-			panic(fmt.Sprintf("license entitlement '%s' signature verification failed: %v", key, err))
+			return fmt.Errorf("license entitlement %q signature verification failed: %w", key, err)
 		}

After these edits, keep a single success info log and proceed to set p.flags.

StephenHinck and others added 7 commits October 6, 2025 09:12
… Concourse to Github Actions (#1921)

* include new config in default config

* signing validation

* validate-image name

* rebase issue

* just prepare

* added an authentication step when validating signed image

* continue on error for validating signed image

* just prepare

---------

Co-authored-by: Lawson Willard <[email protected]>
Co-authored-by: Reuben Lifshay <[email protected]>
* update

* code rabbit suggestions

* update

* fix domainsid not being set for domain nodes from trusts
* chore+ai: reworked error handling and logging
Additional refactoring in an attempt to simplify
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
cmd/api/src/services/dogtags/dogtags.go (2)

35-41: Remove unused ValidateFlag function.

This function is not referenced anywhere in the repository. Since GetAllFlags only iterates over ValidFlags, unknown keys cannot enter the system through that path.

-// ValidateFlag ensures only known flags are used
-func ValidateFlag(key FlagKey) error {
-	if _, exists := ValidFlags[key]; !exists {
-		return fmt.Errorf("unknown dogtag: %s", key)
-	}
-	return nil
-}
-

62-80: Add default case and consider semantic defaults.

Two issues flagged in previous reviews:

  1. Missing default case: The switch has no default case. If an invalid type is added to ValidFlags, the flag will be silently skipped without warning.

  2. Hardcoded defaults: The literal defaults (false, "", 0, 0.0) may not be semantically correct for all flags. For example, ZoneAllocation defaulting to 0 might mean "no zones allowed" rather than an intended default like -1 for "unlimited".

Apply this diff to add a default case:

 func (s *service) GetAllFlags(ctx context.Context) map[FlagKey]interface{} {
 	result := make(map[FlagKey]interface{})
 
 	// Loop through all known flags and get their values from the provider
 	for flagKey, spec := range ValidFlags {
 		switch spec.Type {
 		case "bool":
 			result[flagKey] = s.provider.GetBoolFlag(ctx, string(flagKey), false)
 		case "string":
 			result[flagKey] = s.provider.GetStringFlag(ctx, string(flagKey), "")
 		case "int":
 			result[flagKey] = s.provider.GetIntFlag(ctx, string(flagKey), 0)
 		case "float":
 			result[flagKey] = s.provider.GetFloatFlag(ctx, string(flagKey), 0.0)
+		default:
+			// Log or panic for invalid flag types
+			panic(fmt.Sprintf("unknown flag type %q for flag %q", spec.Type, flagKey))
 		}
 	}
 
 	return result
 }

For semantic defaults, consider extending FlagSpec:

type FlagSpec struct {
	Type         string      `json:"type"`
	Description  string      `json:"description,omitempty"`
	DefaultValue interface{} `json:"-"`
}
🧹 Nitpick comments (1)
cmd/api/src/services/dogtags/providers/yaml.go (1)

88-107: Consider making missing file an error and adding reload capability.

Two design considerations:

  1. Missing file handling: Line 92 returns nil (no error) when the file doesn't exist. This might hide misconfiguration in production where the file path is wrong. Consider logging a warning or making it configurable whether a missing file is an error.

  2. No reload mechanism: Flags are loaded only once during construction. If the YAML file changes at runtime, there's no way to pick up the new values without restarting. Consider adding a Reload() or Watch() method if runtime flag updates are needed.

Example for reload:

// Reload reloads flags from the file
func (p *YAMLProvider) Reload() error {
	return p.loadFlags()
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5685164 and 624492a.

📒 Files selected for processing (4)
  • cmd/api/src/api/v2/dogtags_test.go (1 hunks)
  • cmd/api/src/services/dogtags/dogtags.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/yaml.go (1 hunks)
  • cmd/api/src/services/entrypoint.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/api/src/services/entrypoint.go
  • cmd/api/src/api/v2/dogtags_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/api/src/services/dogtags/dogtags.go (1)
cmd/api/src/services/dogtags/providers/yaml.go (1)
  • NewYAMLProvider (19-34)
cmd/api/src/services/dogtags/providers/yaml.go (1)
cmd/api/src/services/dogtags/dogtags.go (1)
  • NewYAMLProvider (88-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-tests
  • GitHub Check: build-ui
  • GitHub Check: run-tests
  • GitHub Check: run-analysis
🔇 Additional comments (2)
cmd/api/src/services/dogtags/providers/yaml.go (2)

36-46: LGTM!

Thread-safe read with proper type assertion and sensible fallback to default value.


60-73: Good handling of YAML integer parsing nuances.

Correctly handles both int and int64 types since YAML unmarshaling can produce either depending on value size. The conversion ensures consistent return type.

Comment on lines +18 to +34
// NewYAMLProvider creates a new YAML-based dogtags provider
func NewYAMLProvider(filePath string) (*YAMLProvider, error) {
if filePath == "" {
filePath = "local-harnesses/dogtags.yaml"
}

p := &YAMLProvider{
flags: make(map[string]interface{}),
filePath: filePath,
}

if err := p.loadFlags(); err != nil {
return nil, fmt.Errorf("failed to load dogtags: %w", err)
}

return p, nil
}
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

Add path validation to prevent traversal and enforce safe defaults.

The constructor accepts an arbitrary filePath without validation, which could allow path traversal attacks (e.g., ../../etc/passwd) or access to sensitive files. Additionally, the hardcoded default local-harnesses/dogtags.yaml may not exist in production environments.

Consider:

  • Sanitizing the path with filepath.Clean
  • Rejecting paths containing .. or absolute paths
  • Validating file existence and readability before attempting to load
  • Making the default path configurable via environment variable or build flag
+import (
+	"path/filepath"
+	"strings"
+)
+
 func NewYAMLProvider(filePath string) (*YAMLProvider, error) {
 	if filePath == "" {
 		filePath = "local-harnesses/dogtags.yaml"
 	}
+
+	// Sanitize and validate the path
+	cleanPath := filepath.Clean(filePath)
+	if strings.Contains(cleanPath, "..") {
+		return nil, fmt.Errorf("invalid file path: path traversal detected")
+	}
+	if filepath.IsAbs(cleanPath) {
+		return nil, fmt.Errorf("invalid file path: absolute paths not allowed")
+	}
 
 	p := &YAMLProvider{
 		flags:    make(map[string]interface{}),
-		filePath: filePath,
+		filePath: cleanPath,
 	}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cmd/api/src/services/dogtags/providers/yaml.go around lines 18 to 34, the
NewYAMLProvider currently accepts an arbitrary filePath and uses a hardcoded
default; sanitize and validate the path before using it: obtain the default path
from an environment variable (fall back to current default if unset), call
filepath.Clean on the resolved path, reject any path that is absolute or
contains ".." after cleaning, then verify the file exists and is readable with
os.Stat (and optionally os.Open to check permissions) and return a clear error
if the checks fail; only after these validations call p.loadFlags().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
cmd/api/src/services/dogtags/providers/yaml.go (1)

18-33: Sanitize and validate the dogtags file path before loading it.

filePath is consumed verbatim, so a malicious or misconfigured build can point the provider at arbitrary files (../../etc/passwd, /var/secrets/...). Please clean and vet the path (disallow traversal & absolute paths, then store the sanitized value) before calling loadFlags.

@@
-import (
-	"context"
-	"fmt"
-	"os"
-	"sync"
-
-	"gopkg.in/yaml.v3"
-)
+import (
+	"context"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	"gopkg.in/yaml.v3"
+)
@@
 func NewYAMLProvider(filePath string) (*YAMLProvider, error) {
 	if filePath == "" {
 		filePath = "local-harnesses/dogtags.yaml"
 	}
 
+	cleanPath := filepath.Clean(filePath)
+	for _, part := range strings.Split(cleanPath, string(os.PathSeparator)) {
+		if part == ".." {
+			return nil, fmt.Errorf("invalid dogtags file path %q: path traversal detected", filePath)
+		}
+	}
+	if filepath.IsAbs(cleanPath) {
+		return nil, fmt.Errorf("invalid dogtags file path %q: absolute paths are not permitted", filePath)
+	}
+
 	p := &YAMLProvider{
 		flags:    make(map[string]interface{}),
-		filePath: filePath,
+		filePath: cleanPath,
 	}
🧹 Nitpick comments (1)
cmd/api/src/services/dogtags/dogtags.go (1)

65-76: Defend against unknown flag types and make it obvious when specs drift.

Right now an unexpected spec.Type silently drops the flag. Add a default branch (log/return error) so typos or future schema changes are caught immediately.

@@
 	for flagKey, spec := range ValidFlags {
 		switch spec.Type {
 		case "bool":
 			result[flagKey] = s.provider.GetBoolFlag(ctx, string(flagKey), false)
 		case "string":
 			result[flagKey] = s.provider.GetStringFlag(ctx, string(flagKey), "")
 		case "int":
 			result[flagKey] = s.provider.GetIntFlag(ctx, string(flagKey), 0)
 		case "float":
 			result[flagKey] = s.provider.GetFloatFlag(ctx, string(flagKey), 0.0)
+		default:
+			panic(fmt.Sprintf("dogtag %q has unknown type %q", flagKey, spec.Type))
 		}
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 624492a and 14c01cf.

📒 Files selected for processing (2)
  • cmd/api/src/services/dogtags/dogtags.go (1 hunks)
  • cmd/api/src/services/dogtags/providers/yaml.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/api/src/services/dogtags/dogtags.go (1)
cmd/api/src/services/dogtags/providers/yaml.go (1)
  • NewYAMLProvider (19-34)
cmd/api/src/services/dogtags/providers/yaml.go (1)
cmd/api/src/services/dogtags/dogtags.go (1)
  • NewYAMLProvider (88-90)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-tests
  • GitHub Check: build-ui
  • GitHub Check: run-analysis
  • GitHub Check: run-tests

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
cmd/api/src/api/v2/model.go (1)

122-122: LGTM! Consistent dependency injection pattern.

The DogtagsService field addition and constructor changes follow the same pattern as other dependencies in the Resources struct. The initialization is correct and consistent with usage in registration.go.

Since this is a WIP prototype, consider adding nil safety checks in a future iteration if the service is optional, or ensure the caller always provides a valid instance.

Also applies to: 135-135, 150-150

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14c01cf and eb2e139.

📒 Files selected for processing (3)
  • cmd/api/src/api/registration/registration.go (2 hunks)
  • cmd/api/src/api/v2/model.go (5 hunks)
  • cmd/api/src/services/entrypoint.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/api/src/services/entrypoint.go
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/api/src/api/registration/registration.go (1)
cmd/api/src/api/v2/model.go (2)
  • DogtagsService (106-106)
  • NewResources (125-152)
cmd/api/src/api/v2/model.go (1)
cmd/api/src/services/dogtags/dogtags.go (1)
  • Service (44-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: run-analysis
  • GitHub Check: run-tests
  • GitHub Check: build-ui
  • GitHub Check: run-tests
🔇 Additional comments (4)
cmd/api/src/api/registration/registration.go (2)

54-66: LGTM! Clean dependency injection.

The dogtagsService parameter addition follows the established dependency injection pattern used by other services in this function signature.


84-84: Dependency properly threaded through.

The dogtagsService is correctly passed to v2.NewResources, maintaining consistency with the updated constructor signature in model.go.

cmd/api/src/api/v2/model.go (2)

29-29: LGTM! Import added for type alias.

The import is necessary for the DogtagsService type alias introduced below.


105-106: LGTM! Clean type alias pattern.

Using a type alias to expose the dogtags.Service interface is an idiomatic Go pattern that provides a clean boundary between packages while maintaining type safety.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmd/api/src/daemons/changelog/changelog.go (1)

75-86: Poller goroutine isn’t stopped; potential leak on shutdown.

Start launches flagManager.start(ctx) but Stop doesn’t cancel it. If the parent ctx isn’t canceled by the caller, the poller keeps running.

Apply these diffs to manage the poller’s lifecycle locally:

 type Changelog struct {
     options     Options
     flagManager *featureFlagManager
     coordinator *ingestionCoordinator
+    flagCancel  context.CancelFunc
 }
 func (s *Changelog) Start(ctx context.Context) {
-    // Start ingestion coordination
-    s.coordinator.start(ctx, s.options.BatchSize, s.options.FlushInterval)
-
-    // Start feature flag polling
-    s.flagManager.start(ctx)
+    // Start ingestion coordination
+    s.coordinator.start(ctx, s.options.BatchSize, s.options.FlushInterval)
+
+    // Start feature flag polling with a child context we can cancel
+    pollCtx, cancel := context.WithCancel(ctx)
+    s.flagCancel = cancel
+    s.flagManager.start(pollCtx)
 }
 func (s *Changelog) Stop(ctx context.Context) error {
+    if s.flagCancel != nil {
+        s.flagCancel()
+    }
     return s.coordinator.stop(ctx)
 }
cmd/api/src/daemons/changelog/ingestion.go (1)

88-92: Bug: possible data loss on channel close — do a final flush before returning.

If the channel closes before ctx.Done triggers, the loop returns without flushing the buffer.

Apply this diff:

-        case change, ok := <-s.readerC:
-            if !ok {
-                return // channel closed
-            }
+        case change, ok := <-s.readerC:
+            if !ok {
+                // final flush on channel close
+                flushCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+                _ = s.flushBuffer(flushCtx, true)
+                cancel()
+                return
+            }
🧹 Nitpick comments (16)
packages/go/stbernard/command/audit/audit.go (1)

153-160: ExecutionPlan refactor correctly implemented; consider replacing context.TODO().

The refactor to use ExecutionPlan is correct and follows the pattern shown in other analyzers. However, context.TODO() is intended for temporary use during refactoring when the proper context is unclear.

Consider one of these approaches:

Option 1 (Recommended): Thread a context through the call chain:

-func getTicketAudit(env environment.Environment, cwd, startTimestamp, endTimestamp, baseBranch string) (TicketAudit, error) {
+func getTicketAudit(ctx context.Context, env environment.Environment, cwd, startTimestamp, endTimestamp, baseBranch string) (TicketAudit, error) {

Then update the call site in the Run method and use the passed context:

-	result, err := cmdrunner.Run(context.TODO(), executionPlan)
+	result, err := cmdrunner.Run(ctx, executionPlan)

Option 2: If no cancellation/timeout is needed, use context.Background():

-	result, err := cmdrunner.Run(context.TODO(), executionPlan)
+	result, err := cmdrunner.Run(context.Background(), executionPlan)
packages/javascript/bh-shared-ui/src/components/SimpleEnvironmentSelector/SimpleEnvironmentSelector.tsx (1)

150-169: Consider standardizing prop order for consistency.

The two platform selector buttons have their props in different orders. While this doesn't affect functionality, consistent prop ordering improves readability.

Apply this diff to standardize the prop order on the Azure platform button:

                     <li key='azure-platform' className='border-b border-neutral-light-5 pb-2 mb-2'>
                         <Button
+                            className='flex justify-between items-center gap-2 w-full'
                             onClick={handleAzurePlatformClick}
-                            variant={'text'}
                             disabled={disableAZPlatform}
-                            className='flex justify-between items-center gap-2 w-full'>
+                            variant={'text'}>
                             All Azure Tenants
                             <FontAwesomeIcon icon={faCloud} size='sm' />
                         </Button>
                     </li>
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/DefaultRoot.test.tsx (1)

47-47: Consider using a meaningful defaultPath value in the test.

The test passes an empty string (defaultPath='') to DefaultRoot, which may not exercise realistic behavior. Consider using detailsPath or another valid path constant to better reflect actual usage.

Apply this diff:

+import { detailsPath } from '../../routes';
+
 const assetGroupSpy = vi.spyOn(apiClient, 'getAssetGroupTags');
 
 describe('DefaultRoot', () => {
     it('calls getAssetGroupTags for topTagId', () => {
-        render(<DefaultRoot defaultPath='' />);
+        render(<DefaultRoot defaultPath={detailsPath} />);
         waitFor(() => {
             expect(assetGroupSpy).toHaveBeenCalled();
         });
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.test.tsx (1)

83-91: Consider clarifying the test description.

The test logic is correct and properly validates that base64 special characters are percent-encoded. However, the test description "properly encodes + sign" might be slightly misleading since the input string 'hello>world' doesn't contain a + sign—rather, the + appears in the base64-encoded output (aGVsbG8+d29ybGQ=).

Consider renaming to something like:

-    it('properly encodes + sign into the "View in Explore" link', () => {
+    it('properly encodes base64 special characters (e.g., +) in the "View in Explore" link', () => {
.github/workflows/reusable.docker-content-trust.yml (1)

103-105: Make soft-fail opt-in and add a timeout

Defaulting to continue-on-error can mask real breakages. Gate it behind an input and add a job timeout.

Apply this diff in the job:

-    # continue-on-error until validate signed image can be fully validated post-merge
-    continue-on-error: true
+    # Soft-fail is configurable to avoid masking issues
+    continue-on-error: ${{ inputs.allow_validate_failure }}
+    timeout-minutes: 10

And add this input under on.workflow_call.inputs:

allow_validate_failure:
  type: boolean
  default: false
  description: Allow the validate-image job to soft-fail
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/EntitySelectorsInformation.tsx (1)

26-36: Import shared ItemSkeleton and itemSkeletons instead of redefining
Remove the local ItemSkeleton and itemSkeletons in EntitySelectorsInformation.tsx, add import { ItemSkeleton, itemSkeletons } from '../utils', and delete any now-unused Skeleton/CSSProperties imports.

cmd/api/src/daemons/ha/ha.go (4)

25-27: Consider accepting a context in HAMutex.TryLock (or guarantee non-blocking).

Without a ctx, real HA backends may block the poller on network I/O. Either add ctx context.Context to TryLock(ctx) or document that it must be fast/non‑blocking.


32-37: Avoid context.Background() to preserve trace/cancellation where feasible.

Returning Background drops request/service correlation. If you keep the current interface, consider returning a derived/logging context with identifiers; or accept ctx in TryLock (preferred).


29-33: Add compile-time interface conformance for safety.

Ensure dummyHA implements HAMutex.

Apply this diff:

 type dummyHA struct{}
 
+// Ensure dummyHA implements HAMutex
+var _ HAMutex = (*dummyHA)(nil)
+
 func (d *dummyHA) TryLock() (LockResult, error) {

20-23: Exported types lack GoDoc.

Add brief comments to satisfy linters/docs.

cmd/api/src/daemons/changelog/flag_internal_test.go (2)

246-252: Replace fixed sleep with Eventually to reduce flakiness.

Apply this diff:

-        // Wait for polling cycle
-        time.Sleep(100 * time.Millisecond)
-
-        // Cache should now be enabled
-        cache := manager.getCache()
-        require.NotNil(t, cache)
-        require.Equal(t, 1000, cache.getCapacity())
+        // Cache should now be enabled
+        require.Eventually(t, func() bool {
+            c := manager.getCache()
+            return c != nil && c.getCapacity() == 1000
+        }, 500*time.Millisecond, 25*time.Millisecond)

296-303: Replace fixed sleep with Eventually to reduce flakiness.

Apply this diff:

-        // Wait for polling cycle
-        time.Sleep(100 * time.Millisecond)
-
-        // Cache should be enabled since nil HA mutex defaults to primary
-        cache := manager.getCache()
-        require.NotNil(t, cache)
-        require.Equal(t, 1000, cache.getCapacity())
+        // Cache should be enabled since nil HA mutex defaults to primary
+        require.Eventually(t, func() bool {
+            c := manager.getCache()
+            return c != nil && c.getCapacity() == 1000
+        }, 500*time.Millisecond, 25*time.Millisecond)
cmd/api/src/daemons/changelog/flag.go (1)

41-60: Minor: clarify TryLock semantics in comments.

Consider renaming or documenting isPrimary/TryLock semantics (e.g., lease vs. check) to avoid confusion about missing Unlock/renewal.

packages/go/stbernard/analyzers/golang/golang.go (1)

43-49: LGTM: Plan-based execution and exit handling are correct. Consider passing a real ctx.

Use a propagated context instead of context.TODO() when available to enable cancellation/timeouts.

Also applies to: 51-62, 64-64

packages/go/stbernard/workspace/workspace.go (1)

115-121: LGTM: unified plan-based execution. Consider accepting ctx in API.

Change GenerateSchema to accept ctx and pass it to cmdrunner.Run.

Example:

-func GenerateSchema(cwd string, env environment.Environment) error {
+func GenerateSchema(ctx context.Context, cwd string, env environment.Environment) error {
   ...
-  if _, err := cmdrunner.Run(context.TODO(), executionPlan); err != nil {
+  if _, err := cmdrunner.Run(ctx, executionPlan); err != nil {
     return fmt.Errorf("running schemagen: %w", err)
   }
packages/go/stbernard/workspace/golang/build.go (1)

88-94: Migration to ExecutionPlan is correct; consider proper context propagation.

The refactor correctly constructs an ExecutionPlan with all necessary fields and invokes the new Run API. However, using context.TODO() in concurrent goroutines means build operations cannot be cancelled or timed out.

For production-grade build tooling, consider propagating a proper context through BuildMainPackages and buildModuleMainPackages to enable cancellation of long-running builds.

Example enhancement:

func BuildMainPackages(ctx context.Context, workRoot string, modPath string, env environment.Environment) error {
    // ...
    if err := buildModuleMainPackages(ctx, buildDir, modPath, version, env); err != nil {
        return fmt.Errorf("build main packages: %w", err)
    }
    // ...
}

func buildModuleMainPackages(ctx context.Context, buildDir string, modPath string, version semver.Version, env environment.Environment) error {
    // ...
    go func(p GoPackage) {
        defer wg.Done()
        executionPlan := cmdrunner.ExecutionPlan{
            Command: command,
            Args:    args,
            Path:    p.Dir,
            Env:     env.Slice(),
        }
        if _, err := cmdrunner.Run(ctx, executionPlan); err != nil {
            // ...
        }
        // ...
    }(pkg)
    // ...
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb2e139 and a98d855.

⛔ Files ignored due to path filters (1)
  • cmd/ui/public/img/BHCE_Vertical_RedField.svg is excluded by !**/*.svg
📒 Files selected for processing (52)
  • .github/workflows/reusable.docker-content-trust.yml (1 hunks)
  • README.md (1 hunks)
  • cmd/api/src/api/registration/v2.go (2 hunks)
  • cmd/api/src/api/v2/access_control_list.go (1 hunks)
  • cmd/api/src/daemons/changelog/changelog.go (2 hunks)
  • cmd/api/src/daemons/changelog/flag.go (7 hunks)
  • cmd/api/src/daemons/changelog/flag_internal_test.go (8 hunks)
  • cmd/api/src/daemons/changelog/ingestion.go (3 hunks)
  • cmd/api/src/daemons/ha/ha.go (1 hunks)
  • cmd/ui/src/components/LoginPage.tsx (1 hunks)
  • packages/go/ein/ad.go (1 hunks)
  • packages/go/stbernard/analyzers/analyzers.go (4 hunks)
  • packages/go/stbernard/analyzers/golang/golang.go (2 hunks)
  • packages/go/stbernard/analyzers/js/js.go (2 hunks)
  • packages/go/stbernard/cmdrunner/cmdrunner.go (3 hunks)
  • packages/go/stbernard/command/analysis/analysis.go (2 hunks)
  • packages/go/stbernard/command/audit/audit.go (2 hunks)
  • packages/go/stbernard/git/git.go (4 hunks)
  • packages/go/stbernard/workspace/golang/build.go (2 hunks)
  • packages/go/stbernard/workspace/golang/generate.go (1 hunks)
  • packages/go/stbernard/workspace/golang/goimports.go (2 hunks)
  • packages/go/stbernard/workspace/golang/mod.go (2 hunks)
  • packages/go/stbernard/workspace/golang/sync.go (3 hunks)
  • packages/go/stbernard/workspace/golang/tester.go (3 hunks)
  • packages/go/stbernard/workspace/redoc/redoc.go (2 hunks)
  • packages/go/stbernard/workspace/workspace.go (2 hunks)
  • packages/go/stbernard/workspace/yarn/yarn.go (5 hunks)
  • packages/javascript/bh-shared-ui/src/components/AppIcon/Icons/DownArrow.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/components/AppIcon/Icons/LargeRightArrow.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/components/AppIcon/Icons/index.ts (1 hunks)
  • packages/javascript/bh-shared-ui/src/components/FileIngestTable/FileIngestTable.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/components/SimpleEnvironmentSelector/SimpleEnvironmentSelector.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/utils/freeIconsList.ts (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.test.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/DefaultRoot.test.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/DefaultRoot.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/EntitySelectorsInformation.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/TagList.test.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/index.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZones.test.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZones.tsx (4 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZonesContext.tsx (2 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/Summary.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryCard.test.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryCard.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryList.test.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryList.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/index.tsx (1 hunks)
  • packages/javascript/bh-shared-ui/src/views/UserProfile/UserProfile.test.tsx (0 hunks)
  • packages/javascript/bh-shared-ui/src/views/UserProfile/UserProfile.tsx (0 hunks)
💤 Files with no reviewable changes (9)
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/Summary.tsx
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryList.test.tsx
  • packages/javascript/bh-shared-ui/src/views/UserProfile/UserProfile.tsx
  • packages/javascript/bh-shared-ui/src/views/UserProfile/UserProfile.test.tsx
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryCard.tsx
  • packages/javascript/bh-shared-ui/src/components/AppIcon/Icons/DownArrow.tsx
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryCard.test.tsx
  • packages/javascript/bh-shared-ui/src/components/AppIcon/Icons/LargeRightArrow.tsx
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Summary/SummaryList.tsx
✅ Files skipped from review due to trivial changes (4)
  • packages/javascript/bh-shared-ui/src/utils/freeIconsList.ts
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/index.tsx
  • packages/javascript/bh-shared-ui/src/components/FileIngestTable/FileIngestTable.tsx
  • packages/javascript/bh-shared-ui/src/views/DataQuality/DomainInfo.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • cmd/api/src/api/registration/v2.go
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-08T19:01:53.112Z
Learnt from: jvacca-specterops
PR: SpecterOps/BloodHound#1823
File: packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx:108-148
Timestamp: 2025-09-08T19:01:53.112Z
Learning: In BloodHound's CypherSearch component (packages/javascript/bh-shared-ui/src/views/Explore/ExploreSearch/CypherSearch.tsx), the sharing state reset for sharedIds and isPublic after the two-step permissions update is handled elsewhere in the codebase, so additional state reset callbacks in the updateQueryPermissions function are not needed.

Applied to files:

  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.tsx
  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.test.tsx
📚 Learning: 2025-08-28T19:26:03.304Z
Learnt from: benwaples
PR: SpecterOps/BloodHound#1829
File: packages/javascript/bh-shared-ui/src/views/ZoneManagement/ZoneAnalysisIcon.tsx:26-26
Timestamp: 2025-08-28T19:26:03.304Z
Learning: In packages/javascript/bh-shared-ui/src/hooks/, useZonePathParams is exported through the useZoneParams barrel (useZoneParams/index.ts exports it via wildcard from useZonePathParams.tsx), and usePrivilegeZoneAnalysis is exported through useConfiguration.ts. Both are available via the main hooks barrel import.

Applied to files:

  • packages/javascript/bh-shared-ui/src/views/PrivilegeZones/index.tsx
📚 Learning: 2025-05-29T18:24:23.227Z
Learnt from: superlinkx
PR: SpecterOps/BloodHound#1509
File: packages/go/stbernard/command/license/internal/cmd.go:49-51
Timestamp: 2025-05-29T18:24:23.227Z
Learning: In the stbernard package, use slog for all logging prints instead of fmt.Printf/fmt.Fprintf to maintain consistency with the codebase's logging standards.

Applied to files:

  • packages/go/stbernard/command/analysis/analysis.go
📚 Learning: 2025-06-25T17:52:33.291Z
Learnt from: superlinkx
PR: SpecterOps/BloodHound#1606
File: cmd/api/src/analysis/azure/post.go:33-35
Timestamp: 2025-06-25T17:52:33.291Z
Learning: In BloodHound Go code, prefer using explicit slog type functions like slog.Any(), slog.String(), slog.Int(), etc. over simple key-value pairs for structured logging. This provides better type safety and makes key-value pairs more visually distinct. For error types, use slog.Any("key", err) or slog.String("key", err.Error()).

Applied to files:

  • packages/go/stbernard/command/analysis/analysis.go
🧬 Code graph analysis (27)
packages/go/stbernard/analyzers/analyzers.go (1)
packages/go/stbernard/analyzers/codeclimate/codeclimate.go (2)
  • Location (33-36)
  • Lines (39-41)
packages/go/stbernard/command/audit/audit.go (2)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/analyzers/golang/golang.go (1)
  • Run (34-69)
packages/go/stbernard/analyzers/golang/golang.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (3)
  • ExecutionPlan (39-45)
  • Run (75-104)
  • ErrCmdExecutionFailed (36-36)
packages/go/stbernard/workspace/golang/sync.go (2)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/analyzers/golang/golang.go (1)
  • Run (34-69)
packages/go/stbernard/workspace/golang/tester.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/workspace/golang/build.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.tsx (1)
packages/javascript/bh-shared-ui/src/hooks/useExploreGraph/utils.ts (1)
  • encodeCypherQuery (50-52)
cmd/api/src/daemons/changelog/changelog.go (2)
cmd/api/src/daemons/ha/ha.go (2)
  • NewDummyHA (39-41)
  • HAMutex (25-27)
cmd/api/src/model/appcfg/flag.go (1)
  • GetFlagByKeyer (88-91)
cmd/api/src/api/v2/access_control_list.go (1)
cmd/api/src/model/auth.go (1)
  • User (431-451)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/DefaultRoot.tsx (3)
packages/javascript/bh-shared-ui/src/hooks/useAssetGroupTags/useAssetGroupTags.tsx (1)
  • useHighestPrivilegeTagId (454-459)
packages/javascript/bh-shared-ui/src/components/Navigation/AppNavigate.tsx (1)
  • AppNavigate (25-36)
packages/javascript/bh-shared-ui/src/routes/index.tsx (2)
  • privilegeZonesPath (22-22)
  • zonesPath (17-17)
packages/go/stbernard/workspace/redoc/redoc.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/command/analysis/analysis.go (3)
packages/go/stbernard/workspace/workspace.go (1)
  • FindPaths (49-98)
packages/go/stbernard/analyzers/analyzers.go (3)
  • Run (55-76)
  • ErrSeverityExit (32-32)
  • ErrWarnExit (33-33)
packages/go/stbernard/analyzers/js/js.go (1)
  • Run (47-63)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZonesContext.tsx (2)
packages/javascript/js-client-library/src/types.ts (1)
  • AssetGroupTag (85-96)
packages/javascript/bh-shared-ui/src/routes/index.tsx (1)
  • detailsPath (23-23)
packages/go/stbernard/workspace/golang/generate.go (2)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/analyzers/golang/golang.go (1)
  • Run (34-69)
packages/go/stbernard/workspace/golang/goimports.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZones.test.tsx (1)
packages/javascript/bh-shared-ui/src/routes/index.tsx (1)
  • privilegeZonesPath (22-22)
packages/go/stbernard/workspace/golang/mod.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
cmd/api/src/daemons/changelog/flag.go (1)
cmd/api/src/daemons/ha/ha.go (1)
  • HAMutex (25-27)
cmd/api/src/daemons/changelog/flag_internal_test.go (1)
cmd/api/src/daemons/ha/ha.go (2)
  • NewDummyHA (39-41)
  • LockResult (20-23)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
packages/go/stbernard/analyzers/analyzers.go (1)
  • Run (55-76)
packages/go/stbernard/analyzers/golang/golang.go (1)
  • Run (34-69)
packages/go/stbernard/analyzers/js/js.go (2)
packages/go/stbernard/cmdrunner/cmdrunner.go (3)
  • ExecutionPlan (39-45)
  • Run (75-104)
  • ErrCmdExecutionFailed (36-36)
packages/go/stbernard/analyzers/golang/golang.go (1)
  • Run (34-69)
packages/go/stbernard/workspace/yarn/yarn.go (2)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/analyzers/golang/golang.go (1)
  • Run (34-69)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.test.tsx (1)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.tsx (1)
  • Cypher (30-157)
packages/go/ein/ad.go (1)
packages/go/graphschema/ad/ad.go (1)
  • DomainSID (155-155)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/EntitySelectorsInformation.tsx (1)
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/utils.tsx (2)
  • ItemSkeleton (20-30)
  • itemSkeletons (32-32)
packages/go/stbernard/workspace/workspace.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (2)
  • ExecutionPlan (39-45)
  • Run (75-104)
packages/go/stbernard/git/git.go (1)
packages/go/stbernard/cmdrunner/cmdrunner.go (3)
  • ExecutionPlan (39-45)
  • Run (75-104)
  • ErrCmdExecutionFailed (36-36)
🪛 LanguageTool
README.md

[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)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build BloodHound Container Image / Build and Package Container
  • GitHub Check: build-ui
  • GitHub Check: run-tests
  • GitHub Check: run-analysis
  • GitHub Check: run-tests
🔇 Additional comments (37)
cmd/api/src/api/v2/access_control_list.go (1)

61-71: Unused helper: ExtractEnvironmentIDsFromUser No call sites found; confirm whether this function is intended for future use or should be removed.

packages/javascript/bh-shared-ui/src/components/SimpleEnvironmentSelector/SimpleEnvironmentSelector.tsx (3)

170-171: LGTM! Semantic HTML structure improves clarity.

Splitting the platform-level options and individual environments into separate lists is semantically correct and improves the structure. The scrollable container for environments handles overflow well.


176-176: LGTM! Removal of unused index parameter.

Removing the index parameter from the map callback is a good cleanup, as it wasn't being used meaningfully.


178-178: LGTM! Proper use of stable ID for list keys.

Using environment.id as the key is correct and follows React best practices for list rendering with stable identifiers.

cmd/ui/src/components/LoginPage.tsx (1)

44-44: Confirm full-height support. Ensure the Box’s h-full utility takes effect by verifying its parent elements have defined heights (e.g. h-full or h-screen). Tailwind is already configured and used alongside MUI in other components.

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZonesContext.tsx (1)

20-20: LGTM! Context extension is well-structured.

The additions to PrivilegeZonesContextValue (defaultPath and optional Summary) align well with the broader DefaultRoot refactor. Setting defaultPath: detailsPath as the default value is sensible and maintains backward compatibility.

Also applies to: 40-40, 44-44, 50-50

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/TagList.test.tsx (1)

22-22: LGTM! Centralized MSW handlers improve maintainability.

Replacing inline handlers with the centralized zoneHandlers reduces duplication and makes test setup more consistent across the PrivilegeZones test suite.

Also applies to: 86-86

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/index.tsx (1)

19-19: LGTM! Public API extension is appropriate.

Re-exporting SelectedDetails and getSavePath from the Details module follows the standard barrel export pattern and provides a cleaner public API for consumers of the PrivilegeZones module.

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZones.test.tsx (2)

21-21: LGTM! Centralized handlers improve test consistency.

Consolidating MSW handlers into zoneHandlers reduces duplication and ensures consistent mocking across the PrivilegeZones test suite.

Also applies to: 30-30


42-42: LGTM! Simplified route path improves test clarity.

Using the wildcard route path={/${privilegeZonesPath}/*} is cleaner and more flexible than defining multiple specific route patterns in the test setup.

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/DefaultRoot.tsx (1)

22-29: LGTM! Refactor improves flexibility and naming clarity.

Renaming DetailsRoot to DefaultRoot better conveys its role as a catch-all route component. Accepting defaultPath as a prop makes the component more flexible and testable, allowing different consumers to specify their preferred default routing behavior.

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/PrivilegeZones.tsx (3)

42-42: LGTM! Component integration follows the refactor cleanly.

Importing DefaultRoot and destructuring Summary from context aligns with the broader refactor that makes summary routing optional and default path configurable.

Also applies to: 72-72


83-89: LGTM! Conditional routing is well-implemented.

The conditional addition of summary routes based on Summary presence is a clean pattern that allows different consumers of PrivilegeZones to opt in or out of summary functionality.


129-129: LGTM! Context-driven default path maintains consistency.

Passing context.defaultPath to DefaultRoot ensures the catch-all route behavior is consistent with the context configuration, making the component more flexible for different use cases.

packages/go/stbernard/command/analysis/analysis.go (3)

23-23: LGTM!

The log/slog import is necessary for the structured logging on line 90 and aligns with the codebase standard to use slog for all logging.

Based on learnings.


81-84: LGTM!

The explicit error handling for workspace.FindPaths is clear and provides appropriate context in the error message.


86-95: LGTM! Excellent sentinel error handling.

The refactored error handling correctly distinguishes between:

  • High-severity findings (ErrSeverityExit) → returns error to block execution
  • Warning-level findings (ErrWarnExit) → logs warning and allows execution to continue
  • Unexpected errors → returns wrapped error

The control flow is correct: when ErrWarnExit is matched, the function logs the warning and falls through to return nil on line 95, which is the intended behavior for non-blocking warnings.

The use of slog.Warn follows the codebase logging standards. Based on learnings.

packages/javascript/bh-shared-ui/src/components/AppIcon/Icons/index.ts (2)

30-30: LGTM! New exports are correctly integrated.

Both new icon exports (DownArrow and LargeRightArrow) are correctly placed in alphabetical order and follow the established re-export pattern.

Also applies to: 38-38


30-30: No direct bypass imports detected. Removal of default exports for DownArrow and LargeRightArrow is safe.

packages/go/stbernard/analyzers/analyzers.go (1)

70-74: LGTM: Three-way exit logic is correct.

The conditional logic correctly implements the intended behavior:

  • High-severity findings → ErrSeverityExit
  • Minor findings only → ErrWarnExit
  • No findings → nil

This provides useful granularity for CI/CD pipelines to distinguish between errors and warnings.

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.test.tsx (1)

73-81: LGTM!

The updated test expectation correctly reflects the new encoding behavior. Base64 padding characters (=) are now properly percent-encoded as %3D in the URL parameter.

packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/EntitySelectorsInformation.tsx (1)

17-18: LGTM on imports

Imports look correct and needed for the new skeleton helper and existing utilities.

Also applies to: 24-24

cmd/api/src/daemons/changelog/changelog.go (2)

51-61: LGTM: sensible default HA (always-primary) for CE paths.


63-73: LGTM: HA-aware constructor is clean and testable.

cmd/api/src/daemons/changelog/ingestion.go (2)

60-65: LGTM: sane defaulting for invalid flushInterval.


101-107: LGTM: periodic flush path with structured logs.

packages/go/stbernard/cmdrunner/cmdrunner.go (4)

32-34: LGTM: concise short-args limit constant.


36-36: LGTM: error sentinel standardizes exit handling.


38-45: LGTM: ExecutionPlan API is clear and future-proof.


62-71: LGTM: constructor populates result fields from plan.

packages/go/stbernard/git/git.go (6)

50-55: LGTM: .gitmodules existence check with clear errors.


57-66: LGTM: plan-based git config invocation.


85-94: LGTM: warm cache with git status via plan.


96-111: LGTM: correct handling of diff-index exit codes with SuppressErrors.


125-135: LGTM: rev-parse via plan and trimmed output.


153-165: LGTM: timestamp extraction via git log.

packages/go/stbernard/workspace/golang/build.go (1)

20-20: LGTM: Context import added for new API.

The import is necessary for the refactored cmdrunner.Run signature that requires context.Context.

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

Comment on lines +63 to +71
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
}
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.

Comment on lines 86 to 93
case <-ticker.C:
// don't perform the following actions if we aren't the primary instance
active, primaryCtx := s.isPrimary(ctx)
if !active {
continue
}

flagEnabled, size, err := s.flagGetter(ctx)
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

Free cache when losing primary to avoid memory retention on secondaries.

If an instance was primary (cache allocated) and later loses primary, the poller skips work and leaves the cache allocated. Disable to free memory before continuing.

Apply this diff:

-            // don't perform the following actions if we aren't the primary instance
-            active, primaryCtx := s.isPrimary(ctx)
-            if !active {
-                continue
-            }
+            // don't perform mutations if not primary; also free any retained cache
+            active, primaryCtx := s.isPrimary(ctx)
+            if !active {
+                if isEnabled {
+                    s.disable(ctx)
+                    isEnabled = false
+                }
+                continue
+            }
📝 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
case <-ticker.C:
// don't perform the following actions if we aren't the primary instance
active, primaryCtx := s.isPrimary(ctx)
if !active {
continue
}
flagEnabled, size, err := s.flagGetter(ctx)
case <-ticker.C:
// don't perform mutations if not primary; also free any retained cache
active, primaryCtx := s.isPrimary(ctx)
if !active {
if isEnabled {
s.disable(ctx)
isEnabled = false
}
continue
}
flagEnabled, size, err := s.flagGetter(ctx)
🤖 Prompt for AI Agents
In cmd/api/src/daemons/changelog/flag.go around lines 86 to 93, the poller skips
work when the instance is no longer primary but leaves the in-memory flag cache
allocated; change the !active branch so it frees that cache before continuing —
either call an existing cache-clear method (e.g. s.clearFlagCache() or
s.disableFlagCache()) or, if none exists, set the cache field(s) to nil/zero
(for example s.flagCache = nil and s.flagCacheSize = 0) to release memory, then
continue.


parsedData.ExtraNodeProps = append(parsedData.ExtraNodeProps, IngestibleNode{
PropertyMap: map[string]any{"name": trust.TargetDomainName},
PropertyMap: map[string]any{"name": trust.TargetDomainName, ad.DomainSID.String(): trust.TargetDomainSid},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

LGTM! Consider verifying consistency across domain node creation.

The addition of ad.DomainSID.String(): trust.TargetDomainSid to the PropertyMap is appropriate. This makes the DomainSID queryable as a property, which is a common pattern in graph databases where you want both an identifier and a property for the same value.

However, ensure that all other locations where domain nodes are created (e.g., ConvertDomainToNode at line 54) consistently include the DomainSID property to maintain uniformity across the graph.

Run the following script to verify DomainSID property consistency across domain node creation:


🏁 Script executed:

#!/bin/bash
# Description: Find all locations where domain nodes are created and check for DomainSID property usage

# Search for domain node creation patterns
echo "=== Locations creating Domain nodes ==="
ast-grep --pattern $'IngestibleNode{
  $$$
  Labels: []graph.Kind{ad.Domain}
  $$$
}'

echo -e "\n=== PropertyMap assignments with DomainSID ==="
rg -nP 'PropertyMap.*ad\.DomainSID\.String\(\)' --type=go

Length of output: 884


🏁 Script executed:

#!/bin/bash
# Inspect ConvertDomainToNode and itemProps construction for DomainSID usage
rg -nP 'func ConvertDomainToNode' -nC15 packages/go/ein/ad.go
rg -nP 'itemProps\s*[:=]\s*map\[string\]' -nC5 packages/go/ein/ad.go

Length of output: 845


Let’s correct the flags and re-run the search for ConvertDomainToNode and itemProps:


🏁 Script executed:

#!/bin/bash
rg -nP 'func ConvertDomainToNode' -C15 packages/go/ein/ad.go
rg -nP 'itemProps\s*[:=]\s*map\[string\]' -C5 packages/go/ein/ad.go

Length of output: 977


Add DomainSID property to ConvertDomainToNode
ConvertDomainToNode (packages/go/ein/ad.go:54–66) should include

itemProps[ad.DomainSID.String()] = item.ObjectIdentifier

so that all domain nodes expose DomainSID as a property.

🤖 Prompt for AI Agents
In packages/go/ein/ad.go around lines 54–66, ConvertDomainToNode should add the
domain SID as a property on the constructed node; modify the function to set
itemProps[ad.DomainSID.String()] = item.ObjectIdentifier when building itemProps
so that all domain nodes expose DomainSID as a property (mirror how other
properties are added).


var (
ErrSeverityExit = errors.New("high severity linter result")
ErrWarnExit = errors.New("warning severity linter result")
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Breaking change: Update function documentation.

The introduction of ErrWarnExit changes the function's contract. Previously, Run() returned either ErrSeverityExit or nil. Now it can return ErrWarnExit for minor findings, which is a breaking change for callers.

Apply this diff to update the function's godoc:

 // Run all registered analyzers and collects the results into a CodeClimate-like structure
 //
-// If one or more entries have a severity of "error" this function returns an error stating
-// that a high severity result was found.
+// Returns:
+//   - ErrSeverityExit: if one or more entries have severity greater than Minor
+//   - ErrWarnExit: if findings exist but none exceed Minor severity
+//   - nil: if no findings exist
 func Run(paths workspace.WorkspacePaths, env environment.Environment, outputAllSeverity bool) error {

Verify that all callers of Run() handle the new ErrWarnExit return value:


🏁 Script executed:

#!/bin/bash
# Description: Find all callers of analyzers.Run and check their error handling

# Search for function calls to Run in the analyzers package
rg -n --type=go -C 5 'analyzers\.Run\(' -g '!**/analyzers.go'

Length of output: 1035


Update Run() godoc to include ErrWarnExit

Callers in command/analysis already handle ErrWarnExit correctly. Update the function’s documentation in packages/go/stbernard/analyzers/analyzers.go to describe all return values:

 // Run all registered analyzers and collects the results into a CodeClimate-like structure
 //
-// If one or more entries have a severity of "error" this function returns an error stating
-// that a high severity result was found.
+// Returns:
+//   - ErrSeverityExit: if one or more entries have severity greater than Minor
+//   - ErrWarnExit:     if findings exist but none exceed Minor severity
+//   - nil:             if no findings exist
 func Run(paths workspace.WorkspacePaths, env environment.Environment, outputAllSeverity bool) error {
📝 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
ErrWarnExit = errors.New("warning severity linter result")
// Run all registered analyzers and collects the results into a CodeClimate-like structure
//
// Returns:
// - ErrSeverityExit: if one or more entries have severity greater than Minor
// - ErrWarnExit: if findings exist but none exceed Minor severity
// - nil: if no findings exist
func Run(paths workspace.WorkspacePaths, env environment.Environment, outputAllSeverity bool) error {
// … existing implementation …
}
🤖 Prompt for AI Agents
In packages/go/stbernard/analyzers/analyzers.go around line 33, update the Run()
godoc to list and describe all possible return values including ErrWarnExit;
specifically add a short sentence stating that ErrWarnExit is returned when a
linter run produced one or more results with warning severity (and that callers
in command/analysis handle this as a non-fatal warning exit), and ensure the
docblock clearly documents other return cases (nil on success, non-nil error for
fatal failures) so callers understand how to handle each.

Comment on lines +74 to +103
// Run a command with args and environment variables set at a specified path.
func Run(ctx context.Context, ep ExecutionPlan) (*ExecutionResult, error) {
result := newExecutionResult(ep)
cmd := exec.Command(ep.Command, ep.Args...)
cmd.Dir = ep.Path
cmd.Env = ep.Env
cmd.Stdout = io.MultiWriter(result.StandardOutput, result.CombinedOutput)
cmd.Stderr = io.MultiWriter(result.ErrorOutput, result.CombinedOutput)

return &ExecutionError{
ExecutionResult: *result,
}
}
defer logCommand(result, ep.SuppressErrors)()

func (s *ExecutionError) Error() string {
return "command execution failed"
}
debugEnabled := slog.Default().Enabled(ctx, slog.LevelDebug)

func prepareCommand(command string, args []string, path string, env environment.Environment) (*exec.Cmd, *ExecutionResult) {
var (
cmd = exec.Command(command, args...)
result = newExecutionResult(command, args, path)
)
if debugEnabled {
cmd.Stdout = io.MultiWriter(cmd.Stdout, os.Stderr)
cmd.Stderr = io.MultiWriter(cmd.Stderr, os.Stderr)
}

cmd.Dir = path
cmd.Env = env.Slice()
cmd.Stdout = io.MultiWriter(result.StandardOutput, result.CombinedOutput)
cmd.Stderr = io.MultiWriter(result.ErrorOutput, result.CombinedOutput)
if err := cmd.Run(); err != nil {
if exitErr := new(exec.ExitError); errors.As(err, &exitErr) {
result.ReturnCode = exitErr.ExitCode()
if !debugEnabled && !ep.SuppressErrors {
fmt.Fprint(os.Stderr, result.CombinedOutput)
}
return result, fmt.Errorf("%w: %w", ErrCmdExecutionFailed, err)
}
return result, fmt.Errorf("command failed to run: %w", err)
}

return cmd, result
return result, nil
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

Honor ctx cancellation and fix CombinedOutput data race; also log with ctx.

  • Use exec.CommandContext(ctx, ...) so commands terminate on ctx cancel/timeout.
  • CombinedOutput is a bytes.Buffer written by both stdout/stderr concurrently via two goroutines; bytes.Buffer is not thread-safe. Wrap writes with a locked writer.
  • Pass ctx into logCommand and use slog.*Context.

Apply these diffs:

Run: use CommandContext, locked writer, and pass ctx to logCommand.

 func Run(ctx context.Context, ep ExecutionPlan) (*ExecutionResult, error) {
   result := newExecutionResult(ep)
-  cmd := exec.Command(ep.Command, ep.Args...)
+  cmd := exec.CommandContext(ctx, ep.Command, ep.Args...)
   cmd.Dir = ep.Path
   cmd.Env = ep.Env
-  cmd.Stdout = io.MultiWriter(result.StandardOutput, result.CombinedOutput)
-  cmd.Stderr = io.MultiWriter(result.ErrorOutput, result.CombinedOutput)
+  combined := &lockedBuffer{buf: result.CombinedOutput}
+  cmd.Stdout = io.MultiWriter(result.StandardOutput, combined)
+  cmd.Stderr = io.MultiWriter(result.ErrorOutput, combined)
 
-  defer logCommand(result, ep.SuppressErrors)()
+  defer logCommand(ctx, result, ep.SuppressErrors)()

logCommand: accept ctx, use context-aware logging.

-func logCommand(result *ExecutionResult, suppressErrors bool) func() {
+func logCommand(ctx context.Context, result *ExecutionResult, suppressErrors bool) func() {
   commandStr := shortCommandString(result.Command, result.Arguments, maxShortArgs)
   started := time.Now()
-
-  slog.Info("exec", slog.String("command", commandStr))
+  slog.InfoContext(ctx, "exec", slog.String("command", commandStr))
 
   return func() {
     elapsed := time.Since(started)
     logLevel := slog.LevelDebug
     timeUnit := elapsed.Milliseconds()
 
     if result.ReturnCode != 0 && !suppressErrors {
       logLevel = slog.LevelError
     }
 
-    slog.Log(context.TODO(), logLevel, "exec result",
+    slog.Log(ctx, logLevel, "exec result",
       slog.String("command", result.Command),
       slog.String("args", strings.Join(result.Arguments, " ")),
       slog.String("path", result.Path),
       slog.Int("return_code", result.ReturnCode),
       slog.Int64("elapsed_ms", timeUnit),
     )
   }
 }

Add the lockedBuffer (outside the above ranges):

// thread-safe writer around a bytes.Buffer
type lockedBuffer struct {
    mu  sync.Mutex
    buf *bytes.Buffer
}

func (l *lockedBuffer) Write(p []byte) (int, error) {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.buf.Write(p)
}

And include the import:

 import (
   "bytes"
   "context"
   "errors"
   "fmt"
   "io"
   "log/slog"
   "os"
   "os/exec"
   "strings"
   "time"
+  "sync"
 )

Also applies to: 124-146

Comment on lines +68 to 76
for _, keyValueStr := range strings.Split(strings.TrimSpace(result.StandardOutput.String()), "\n") {
keyValue := strings.Split(keyValueStr, " ")

if len(keyValue) != 2 {
return subPaths, fmt.Errorf("%w: %s", ErrInvalidConfigValue, keyValueStr)
}

return subPaths, nil
subPaths = append(subPaths, filepath.Join(cwd, keyValue[1]))
}
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

Handle submodule paths containing spaces.

strings.Split will mis-parse when the path contains spaces. Use SplitN with limit 2.

-    keyValue := strings.Split(keyValueStr, " ")
+    keyValue := strings.SplitN(keyValueStr, " ", 2)
📝 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
for _, keyValueStr := range strings.Split(strings.TrimSpace(result.StandardOutput.String()), "\n") {
keyValue := strings.Split(keyValueStr, " ")
if len(keyValue) != 2 {
return subPaths, fmt.Errorf("%w: %s", ErrInvalidConfigValue, keyValueStr)
}
return subPaths, nil
subPaths = append(subPaths, filepath.Join(cwd, keyValue[1]))
}
for _, keyValueStr := range strings.Split(strings.TrimSpace(result.StandardOutput.String()), "\n") {
keyValue := strings.SplitN(keyValueStr, " ", 2)
if len(keyValue) != 2 {
return subPaths, fmt.Errorf("%w: %s", ErrInvalidConfigValue, keyValueStr)
}
subPaths = append(subPaths, filepath.Join(cwd, keyValue[1]))
}
🤖 Prompt for AI Agents
In packages/go/stbernard/git/git.go around lines 68 to 76, the current parsing
uses strings.Split(keyValueStr, " ") which breaks when the submodule path
contains spaces; change to use strings.SplitN(keyValueStr, " ", 2) to only split
into key and the rest (path), keep the existing len check for 2, and continue to
append filepath.Join(cwd, keyValue[1]) so paths with spaces are preserved; also
ignore empty lines by skipping keyValueStr == "" or trimming it before
splitting.

Comment on lines 80 to 84
const exploreUrl = useMemo(
() => `/ui/explore?searchType=cypher&exploreSearchTab=cypher&cypherSearch=${encodeCypherQuery(cypherQuery)}`,
() =>
`/ui/explore?searchType=cypher&exploreSearchTab=cypher&cypherSearch=${encodeURIComponent(encodeCypherQuery(cypherQuery))}`,
[cypherQuery]
);
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

🧩 Analysis chain

Verify the Explore page correctly decodes the cypherSearch parameter.

The change to add encodeURIComponent wrapping is correct and ensures URL-safe encoding of the base64 query parameter. Base64 can contain characters like +, /, and = which have special meaning in URLs.

Please verify that the Explore page correctly decodes the cypherSearch parameter by first calling decodeURIComponent and then atob (or your base64 decode function). Run the following script to check the implementation:


🏁 Script executed:

#!/bin/bash
# Description: Verify that the Explore page correctly decodes the cypherSearch parameter.
# Expected: The code should call decodeURIComponent before decoding the base64 string.

# Search for cypherSearch parameter usage in Explore-related files
rg -n -A 5 -B 2 "cypherSearch" --type=ts --type=tsx -g "*Explore*" -g "!*.test.*"

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Locate TS/TSX files with "Explore" in their path
explore_files=$(fd --type f --extension ts --extension tsx Explore) || true

if [ -z "$explore_files" ]; then
  echo "No Explore-related .ts/.tsx files found"
  exit 0
fi

echo "== Explore files found =="
printf '%s\n' "$explore_files"

echo
echo "== cypherSearch usage =="
rg -n "cypherSearch" -C3 $explore_files || true

echo
echo "== decodeURIComponent usage =="
rg -n "decodeURIComponent" -C3 $explore_files || true

echo
echo "== base64 decode usage (atob or Buffer) =="
rg -nP "atob|Buffer\\.from" -C3 $explore_files || true

Length of output: 16618


Decode cypherSearch before use

In packages/javascript/bh-shared-ui/src/hooks/useExploreParams/useExploreParams.tsx, the URL-param cypherSearch is currently read via searchParams.get('cypherSearch') but never decoded. Wrap that value in decodeURIComponent and then base64-decode it (e.g. atob) so downstream code receives the original Cypher query.

🤖 Prompt for AI Agents
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/Cypher/Cypher.tsx
around lines 80 to 84: the cypherSearch param is currently URL-encoded but not
base64-encoded, while the reader expects a decodeURIComponent followed by base64
decoding (atob); update the URL construction to base64-encode the raw cypher
query before URL-encoding it (e.g. wrap the query with btoa(...) and then
encodeURIComponent, or ensure encodeCypherQuery returns a base64 string and
still call encodeURIComponent on that) so downstream useExploreParams can decode
via decodeURIComponent + atob to recover the original Cypher text.


const assetGroupSpy = vi.spyOn(apiClient, 'getAssetGroupTags');

describe('DetailsRoot', () => {
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

Update the test description to match the renamed component.

The test description still references DetailsRoot, but the component was renamed to DefaultRoot.

Apply this diff:

-describe('DetailsRoot', () => {
+describe('DefaultRoot', () => {
📝 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
describe('DetailsRoot', () => {
describe('DefaultRoot', () => {
🤖 Prompt for AI Agents
In
packages/javascript/bh-shared-ui/src/views/PrivilegeZones/DefaultRoot.test.tsx
around line 45, the test suite description still uses "DetailsRoot" but the
component has been renamed to "DefaultRoot"; update the describe string to
"DefaultRoot" (or an equivalent accurate name) so the test name matches the
component rename.

Comment on lines +11 to +13
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).
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.

@kpom-specter kpom-specter changed the title [DRAFT] [WIP] DogTags [POC] DogTags Oct 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.