-
Notifications
You must be signed in to change notification settings - Fork 244
[POC] DogTags #1947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
[POC] DogTags #1947
Conversation
…different sources
WalkthroughAdds 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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
: WireDogtagsService
intoNewResources
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 meansGetDogtags
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 newType
, the loop quietly omits that flag and the API response silently regresses. Tighten this by handling thedefault
branch—either return an error (which means wideningGetAllFlags
/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 adefer 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
andDogtagEvaluation
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 inGetFloatFlag
.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. Convertint
/int64
tofloat64
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
📒 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
routerInst.GET("/api/v2/dogtags", resources.GetDogtags), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard /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.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is startup level application settings. What options we even show the user to login, or if the application starts up at all should be listed here. I'm not sure that these should be private? Human talk needed.
There was a problem hiding this comment.
Choose a reason for hiding this 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
andapi_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:
- Truly public flags (login methods, basic health) → public endpoint
- Internal/operational flags (connection limits, URLs, etc.) → authenticated endpoint
OR
- 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.
func NewProvider(filePath string) (*NoOpProvider, error) { | ||
return &NoOpProvider{}, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
* 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]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 returnsmap[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:
- Always return defaults on failure (hiding errors)
- 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 therawProvider
interface stating that all implementations must support concurrent use.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 flexibilityNote: Ensure
config.FilePath
is validated before calling this function (see earlier comment on lines 10-13).
// Config for dogtags service | ||
type Config struct { | ||
FilePath string `yaml:"file_path"` | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
📒 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.
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)) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
.
… 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 overValidFlags
, 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:
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.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:
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.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()
orWatch()
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
📒 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
andint64
types since YAML unmarshaling can produce either depending on value size. The conversion ensures consistent return type.
// 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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 callingloadFlags
.@@ -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
📒 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 theResources
struct. The initialization is correct and consistent with usage inregistration.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
📒 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 tov2.NewResources
, maintaining consistency with the updated constructor signature inmodel.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.
* feat: add scroll to simple domain selector * feat: added aggregate options on top of list & reduced max height * fix: removed unused var
…different sources
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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=''
) toDefaultRoot
, which may not exercise realistic behavior. Consider usingdetailsPath
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 timeoutDefaulting 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: 10And add this input under
on.workflow_call.inputs
:allow_validate_failure: type: boolean default: false description: Allow the validate-image job to soft-failpackages/javascript/bh-shared-ui/src/views/PrivilegeZones/Details/EntitySelectorsInformation.tsx (1)
26-36
: Import shared ItemSkeleton and itemSkeletons instead of redefining
Remove the localItemSkeleton
anditemSkeletons
inEntitySelectorsInformation.tsx
, addimport { ItemSkeleton, itemSkeletons } from '../utils'
, and delete any now-unusedSkeleton
/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
toTryLock(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
implementsHAMutex
.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 newRun
API. However, usingcontext.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
andbuildModuleMainPackages
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
⛔ 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’sh-full
utility takes effect by verifying its parent elements have defined heights (e.g.h-full
orh-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. SettingdefaultPath: 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
andgetSavePath
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
toDefaultRoot
better conveys its role as a catch-all route component. AcceptingdefaultPath
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 destructuringSummary
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
toDefaultRoot
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 returnnil
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
andLargeRightArrow
) 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 importsImports 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 requirescontext.Context
.
- run: | | ||
echo "Validating signature of image..." | ||
docker pull ${{ inputs.dockerhub_image_reference }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against digest refs and add explicit trust inspect
Notary v1 trust is tag-based; digest references won’t validate signatures as intended. Enforce tag usage and inspect trust metadata explicitly.
Apply this diff to harden the validation step:
- - 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.
- 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.
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against 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.
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.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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.
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.
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
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])) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
const exploreUrl = useMemo( | ||
() => `/ui/explore?searchType=cypher&exploreSearchTab=cypher&cypherSearch=${encodeCypherQuery(cypherQuery)}`, | ||
() => | ||
`/ui/explore?searchType=cypher&exploreSearchTab=cypher&cypherSearch=${encodeURIComponent(encodeCypherQuery(cypherQuery))}`, | ||
[cypherQuery] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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
Checklist:
Summary by CodeRabbit