Skip to content

Conversation

@brandonshearin
Copy link
Contributor

@brandonshearin brandonshearin commented Jan 16, 2026

Description

  • The majority of the changes are in api/v2/search.go where the ListAvailableEnvironments resources handler lives. This function was previously called GetAvailableDomains and the verbiage has been updated from domains -> environments to better reflect its purpose.
  • Add an opengraph_findings feature flag, non user-updatable. When on, api/v2/available-domains will return any environments registered through OpenGraph in addition to existing ad domains and azure tenants.
  • Decorates the model.SchemaEnvironment result of the GetEnvironments DB method with 2 virtual fields-- extension name and environment kind name.

Motivation and Context

BED-6761
We want environments collected under a registered OpenGraph extension to be selectable in the UI. This requires updates to the existing api/v2/available-domains endpoint.

How Has This Been Tested?

  • all existing tests have been updated and are passing.

Screenshots (optional):

Types of changes

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

Checklist:

Summary by CodeRabbit

  • New Features

    • Environment list responses now include environment kind names, schema extension display names, and richer metadata (collected status, impact value, hygiene attack paths, exposures).
    • Added "OpenGraph Findings" feature flag to control OpenGraph-based environment discovery.
  • Refactor

    • Renamed domain-focused endpoints and types to environment-centric names for clarity.
  • Chores

    • Database migration added to register the new feature flag.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

Walkthrough

Renames domain selectors to environment selectors, expands selector fields, replaces GET /api/v2/available-domains with a DB-driven ListAvailableEnvironments flow (including BuildEnvironmentFilter/BuildEnvironmentSelectors), adds an OpenGraph feature flag, and extends SchemaEnvironment with display-name fields.

Changes

Cohort / File(s) Summary
Type & Model Changes
cmd/api/src/model/search.go, cmd/api/src/model/search_test.go, cmd/api/src/model/graphschema.go, cmd/api/src/model/appcfg/flag.go
Renamed DomainSelector(s)EnvironmentSelector(s), added fields (Collected, ImpactValue, HygieneAttackPaths, Exposures), adjusted GetFilterCriteria signature to accept []graph.Kind, added FeatureOpenGraphFindings constant, and added display-name fields to SchemaEnvironment.
API: handler + pipeline
cmd/api/src/api/v2/search.go, cmd/api/src/api/v2/search_test.go, cmd/api/src/api/v2/search_internal_test.go, cmd/api/src/api/registration/v2.go, cmd/api/src/api/sort_test.go
Replaced GetAvailableDomains with ListAvailableEnvironments; introduced BuildEnvironmentFilter, BuildEnvironmentSelectors, plus resolve helpers; updated handler to call DB for environments and feature flag, query graph, build/select and respond; updated tests and mocks to exercise DB and flag interactions.
Database layer & migration
cmd/api/src/database/graphschema.go, cmd/api/src/database/graphschema_integration_test.go, cmd/api/src/database/migration/migrations/v8.6.0.sql
Rewrote GetEnvironments to use explicit SQL joins to populate schema extension and kind display names; updated integration tests to expect new fields; added migration row for opengraph_findings feature flag.
Minor config / formatting
cmd/api/src/config/config.go, cmd/api/src/config/default.go
Whitespace/formatting adjustments around DisableAPIKeys initialization (no semantic change).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant APIHandler as ListAvailableEnvironments Handler
    participant DB as Database
    participant Graph as Graph Engine

    Client->>APIHandler: GET /api/v2/available-domains
    APIHandler->>DB: GetEnvironments(ctx)
    DB-->>APIHandler: []SchemaEnvironment
    APIHandler->>DB: GetFlagByKey(ctx, "opengraph_findings")
    DB-->>APIHandler: FeatureFlag
    APIHandler->>APIHandler: BuildEnvironmentFilter(ctx, db, request)
    APIHandler->>Graph: GraphQuery(filter criteria)
    Graph-->>APIHandler: []*graph.Node
    APIHandler->>APIHandler: BuildEnvironmentSelectors(nodes, kindToDisplayName)
    APIHandler-->>Client: JSON array of EnvironmentSelectors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

api, enhancement, dbmigration

Suggested reviewers

  • AD7ZJ
  • cweidenkeller
  • urangel

Poem

🐰 With twitching whiskers and a hop so spry,

I swapped domains for environments high.
Flags and kinds all lined in a row,
The schema blooms and selectors grow.
Hooray — a carrot-toast for this new flow! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: renaming GetAvailableDomains to ListAvailableEnvironments and adding OpenGraph Environments support, with the associated ticket BED-6761.
Description check ✅ Passed The description covers main changes (handler rename, feature flag, schema decoration) and includes motivation (BED-6761) and testing statement, but checklist items remain unchecked and related sections are incomplete or lack detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@cmd/api/src/api/v2/search.go`:
- Around line 150-168: The resolveEnvType function currently returns an empty
string when no kind matches, which can break consumers; update resolveEnvType to
return a stable fallback (e.g., "unknown") instead of "" and emit a warning via
the existing logger or a passed-in logger if available; locate resolveEnvType
and modify its final return path (after checking node.Kinds and
kindToDisplayName) to log a warning mentioning node.ID or node.Kinds and then
return "unknown" so API/UI consumers always receive a non-empty environment
type.

In `@cmd/api/src/database/graphschema.go`:
- Around line 869-894: Delete the dead code: remove the Kind struct declaration
and the GetKindByName method on BloodhoundDB (including the TODO comment above
it) since they are unused; ensure no remaining references to Kind or
GetKindByName remain (and remove any now-unused ErrNotFound or other
imports/vars only referenced by that method) and run tests/compile to confirm
nothing else depended on these symbols.
🧹 Nitpick comments (4)
cmd/api/src/model/search_test.go (1)

30-34: Consider renaming test functions to match type name.

The test function names retain DomainSelectors prefix (e.g., TestDomainSelectors_TestIsSortable) while the type is now EnvironmentSelectors. Consider renaming for consistency—e.g., TestEnvironmentSelectors_IsSortable.

cmd/api/src/api/v2/search_test.go (1)

236-249: Consider adding a test case for FeatureOpenGraphFindings enabled.

All test cases mock FeatureOpenGraphFindings as disabled. Consider adding a test case where this flag is enabled to verify the alternate code path behavior.

Also applies to: 268-269, 298-299

cmd/api/src/model/search.go (1)

89-95: Consider using the shared GetFilterableColumns helper from cmd/api/src/api/filters.go.

The relevant code snippets show there's already a GetFilterableColumns function in cmd/api/src/api/filters.go that performs the same logic. This implementation duplicates that code. However, since EnvironmentSelectors may need to implement a specific interface, this might be intentional.

cmd/api/src/api/v2/search.go (1)

139-148: Consider adding a comment explaining the rationale for defaulting OpenGraph extensions to collected=true.

The logic is clear for built-ins (respect the stored property), but the decision to default OpenGraph extensions to true may warrant a brief explanation beyond "for now" to help future maintainers understand the business requirement.

Comment on lines +150 to +168
func resolveEnvType(node *graph.Node, kindToDisplayName map[string]string) string {
// TODO: Remove hardcoded built-in types once they are saved in DB and not CUE
if node.Kinds.ContainsOneOf(azure.Tenant) {
return "azure"
}
if node.Kinds.ContainsOneOf(ad.Domain) {
return "active-directory"
}

// For custom extensions, use the display name from the schema extension
// Note: Nodes should only have one environment kind. In the edge case where there are multiple, we take the first.
for _, kind := range node.Kinds {
if displayName, ok := kindToDisplayName[kind.String()]; ok {
return displayName
}
}

return ""
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty string fallback for unknown environment types may cause UI/API issues.

When a node doesn't match any known kind in kindToDisplayName, the function returns an empty string. This could result in confusing API responses where type is blank. Consider logging a warning or returning a fallback like "unknown".

Suggested improvement
 	// For custom extensions, use the display name from the schema extension
 	// Note: Nodes should only have one environment kind. In the edge case where there are multiple, we take the first.
 	for _, kind := range node.Kinds {
 		if displayName, ok := kindToDisplayName[kind.String()]; ok {
 			return displayName
 		}
 	}

-	return ""
+	return "unknown"
📝 Committable suggestion

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

Suggested change
func resolveEnvType(node *graph.Node, kindToDisplayName map[string]string) string {
// TODO: Remove hardcoded built-in types once they are saved in DB and not CUE
if node.Kinds.ContainsOneOf(azure.Tenant) {
return "azure"
}
if node.Kinds.ContainsOneOf(ad.Domain) {
return "active-directory"
}
// For custom extensions, use the display name from the schema extension
// Note: Nodes should only have one environment kind. In the edge case where there are multiple, we take the first.
for _, kind := range node.Kinds {
if displayName, ok := kindToDisplayName[kind.String()]; ok {
return displayName
}
}
return ""
}
func resolveEnvType(node *graph.Node, kindToDisplayName map[string]string) string {
// TODO: Remove hardcoded built-in types once they are saved in DB and not CUE
if node.Kinds.ContainsOneOf(azure.Tenant) {
return "azure"
}
if node.Kinds.ContainsOneOf(ad.Domain) {
return "active-directory"
}
// For custom extensions, use the display name from the schema extension
// Note: Nodes should only have one environment kind. In the edge case where there are multiple, we take the first.
for _, kind := range node.Kinds {
if displayName, ok := kindToDisplayName[kind.String()]; ok {
return displayName
}
}
return "unknown"
}
🤖 Prompt for AI Agents
In `@cmd/api/src/api/v2/search.go` around lines 150 - 168, The resolveEnvType
function currently returns an empty string when no kind matches, which can break
consumers; update resolveEnvType to return a stable fallback (e.g., "unknown")
instead of "" and emit a warning via the existing logger or a passed-in logger
if available; locate resolveEnvType and modify its final return path (after
checking node.Kinds and kindToDisplayName) to log a warning mentioning node.ID
or node.Kinds and then return "unknown" so API/UI consumers always receive a
non-empty environment type.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@cmd/api/src/api/v2/search.go`:
- Around line 86-116: The error branch in ListAvailableEnvironments is inverted:
when BuildEnvironmentFilter returns database.ErrNotFound treat it as a
server/configuration issue (respond with an internal server error or not-found
status) instead of calling api.HandleDatabaseError for other errors; update the
logic in ListAvailableEnvironments so that errors.Is(err, database.ErrNotFound)
triggers api.WriteErrorResponse with http.StatusInternalServerError (including
err.Error() in the message) and all other filter errors return
http.StatusBadRequest via api.WriteErrorResponse; reference
BuildEnvironmentFilter, database.ErrNotFound, ListAvailableEnvironments,
api.WriteErrorResponse and ensure the response uses the actual error text for
debugging.
♻️ Duplicate comments (1)
cmd/api/src/api/v2/search.go (1)

150-168: Empty string fallback for unknown environment types may cause issues.

When a node doesn't match any known kind in kindToDisplayName, the function returns an empty string. This could result in confusing API responses where type is blank. Consider returning a fallback like "unknown" or logging a warning.

Suggested improvement
 	// For custom extensions, use the display name from the schema extension
 	// Note: Nodes should only have one environment kind. In the edge case where there are multiple, we take the first.
 	for _, kind := range node.Kinds {
 		if displayName, ok := kindToDisplayName[kind.String()]; ok {
 			return displayName
 		}
 	}

-	return ""
+	return "unknown"
🧹 Nitpick comments (2)
cmd/api/src/api/v2/search_test.go (1)

234-243: Consider adding a test case for GetEnvironments database error.

The current error test only covers GetFilteredAndSortedNodes failure. A test case for when GetEnvironments returns an error would improve coverage of the error handling path in BuildEnvironmentFilter.

Suggested test case
{
    Name: "GetEnvironmentsError",
    Setup: func() {
        mockDB.EXPECT().GetEnvironments(gomock.Any()).Return(nil, fmt.Errorf("database error"))
    },
    Test: func(output apitest.Output) {
        apitest.StatusCode(output, http.StatusBadRequest)
    },
},
cmd/api/src/api/v2/search.go (1)

139-148: Clarify the rationale for OpenGraph extensions defaulting to collected: true.

The comment says "all OpenGraph extensions default to true for now" but doesn't explain why. This behavior differs from built-ins which respect the actual collected property. Consider adding a brief explanation or TODO for future alignment.

Comment on lines 86 to 116
func (s *Resources) ListAvailableEnvironments(response http.ResponseWriter, request *http.Request) {
ctx := request.Context()

if sortItems, err := api.ParseGraphSortParameters(domains, request.URL.Query()); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsNotSortable, request), response)
} else if filterCriteria, err := domains.GetFilterCriteria(request); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
} else if nodes, err := s.GraphQuery.GetFilteredAndSortedNodes(sortItems, filterCriteria); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("%s: %s", api.ErrorResponseDetailsInternalServerError, err), request), response)
} else {
api.WriteBasicResponse(request.Context(), setNodeProperties(nodes), http.StatusOK, response)
sortItems, err := api.ParseGraphSortParameters(model.EnvironmentSelectors{}, request.URL.Query())
if err != nil {
api.WriteErrorResponse(ctx, api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsNotSortable, request), response)
return
}

filterResult, err := BuildEnvironmentFilter(ctx, s.DB, request)
if err != nil {
if errors.Is(err, database.ErrNotFound) {
api.HandleDatabaseError(request, response, err)
} else {
api.WriteErrorResponse(ctx, api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
}
return
}

// Fetch and filter domain nodes
nodes, err := s.GraphQuery.GetFilteredAndSortedNodes(sortItems, filterResult.FilterCriteria)
if err != nil {
api.WriteErrorResponse(ctx, api.BuildErrorResponse(http.StatusInternalServerError, err.Error(), request), response)
return
}

// Build response with domain type display names
responseData := BuildEnvironmentSelectors(nodes, filterResult.KindToDisplayName)

api.WriteBasicResponse(ctx, responseData, http.StatusOK, response)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error handling for ErrNotFound appears inverted.

When BuildEnvironmentFilter returns database.ErrNotFound, the code calls HandleDatabaseError, but for other errors it writes a StatusBadRequest. This seems backward - a database "not found" error during filter building typically indicates a configuration issue (not a client error), while filter parsing errors (like invalid query params) would be bad requests.

Consider whether the logic should be:

  • ErrNotFoundStatusInternalServerError or StatusNotFound (server/config issue)
  • Other filter errors → StatusBadRequest (client input issue)
🔧 Suggested fix
 	filterResult, err := BuildEnvironmentFilter(ctx, s.DB, request)
 	if err != nil {
-		if errors.Is(err, database.ErrNotFound) {
-			api.HandleDatabaseError(request, response, err)
-		} else {
+		if errors.Is(err, database.ErrNotFound) {
+			// Environment configuration not found - server/config issue
 			api.WriteErrorResponse(ctx, api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
+		} else {
+			api.HandleDatabaseError(request, response, err)
 		}
 		return
 	}
🤖 Prompt for AI Agents
In `@cmd/api/src/api/v2/search.go` around lines 86 - 116, The error branch in
ListAvailableEnvironments is inverted: when BuildEnvironmentFilter returns
database.ErrNotFound treat it as a server/configuration issue (respond with an
internal server error or not-found status) instead of calling
api.HandleDatabaseError for other errors; update the logic in
ListAvailableEnvironments so that errors.Is(err, database.ErrNotFound) triggers
api.WriteErrorResponse with http.StatusInternalServerError (including
err.Error() in the message) and all other filter errors return
http.StatusBadRequest via api.WriteErrorResponse; reference
BuildEnvironmentFilter, database.ErrNotFound, ListAvailableEnvironments,
api.WriteErrorResponse and ensure the response uses the actual error text for
debugging.

@brandonshearin brandonshearin changed the title Bed 6761 2 feat: Available Domains updated to support OG Environments: BED-6761 Jan 16, 2026
@brandonshearin brandonshearin self-assigned this Jan 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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/model/search.go (1)

72-81: Remove collected from the IsString() method since it's a boolean field.

The collected field is defined as bool (line 39), but IsString() incorrectly returns true for it. This causes filter parsing to treat the value as a string (wrapping it in single quotes as 'true'), rather than using the proper boolean comparison with IS/IS NOT operators. Filters on the collected field will generate incorrect queries as a result.

Suggested fix
 func (s EnvironmentSelectors) IsString(column string) bool {
 	switch column {
 	case "name",
-		"objectid",
-		"collected":
+		"objectid":
 		return true
 	default:
 		return false
 	}
 }
🤖 Fix all issues with AI agents
In `@cmd/api/src/api/v2/search.go`:
- Around line 134-143: The resolve is that resolveCollected currently forces
non-AD/Azure nodes to always return true; change resolveCollected to respect the
actual collected property for all node kinds by returning the boolean from
node.Properties.GetOrDefault(common.Collected.String(), false).Bool() (instead
of the hardcoded true) so OpenGraph environments reflect their real state; if
the always-true behavior was intentional add a TODO comment and link to a
tracking issue id in the function (resolveCollected) explaining it's temporary
and why.
♻️ Duplicate comments (2)
cmd/api/src/api/v2/search.go (2)

85-98: Error handling simplified but logic is clearer now.

The past review comment about inverted error handling for ErrNotFound no longer applies since the code now uses a single error response path. However, note that all errors from BuildEnvironmentFilter (including DB errors from GetEnvironments or GetFlagByKey) will return StatusBadRequest, which may not be appropriate for internal/DB errors.

Consider distinguishing between client-side errors (bad filter params) and server-side errors (DB failures) for more accurate HTTP status codes.


145-163: Empty string fallback for unknown environment types may cause UI/API issues.

This concern was raised in a past review. When a node doesn't match any known kind in kindToDisplayName, the function returns an empty string, which could result in confusing API responses where type is blank.

Suggested improvement
 	// For custom extensions, use the display name from the schema extension
 	// Note: Nodes should only have one environment kind. In the edge case where there are multiple, we take the first.
 	for _, kind := range node.Kinds {
 		if displayName, ok := kindToDisplayName[kind.String()]; ok {
 			return displayName
 		}
 	}

-	return ""
+	return "unknown"

Comment on lines +134 to +143
func resolveCollected(node *graph.Node) bool {
collected, _ := node.Properties.GetOrDefault(common.Collected.String(), false).Bool()
// builtin Extensions should respect the collected property as always
if node.Kinds.ContainsOneOf(azure.Tenant) || node.Kinds.ContainsOneOf(ad.Domain) {
return collected
} else {
// all OpenGraph extensions default to true for now
return true
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

OpenGraph environments always return collected: true regardless of actual state.

The hardcoded return true for non-AD/Azure environments means the UI will always show OpenGraph environments as collected, even if they aren't. The comment says "for now," but this could mislead users.

Consider adding a TODO or tracking issue if this is intentional temporary behavior, or reading the actual collected property for OpenGraph environments as well.

🤖 Prompt for AI Agents
In `@cmd/api/src/api/v2/search.go` around lines 134 - 143, The resolve is that
resolveCollected currently forces non-AD/Azure nodes to always return true;
change resolveCollected to respect the actual collected property for all node
kinds by returning the boolean from
node.Properties.GetOrDefault(common.Collected.String(), false).Bool() (instead
of the hardcoded true) so OpenGraph environments reflect their real state; if
the always-true behavior was intentional add a TODO comment and link to a
tracking issue id in the function (resolveCollected) explaining it's temporary
and why.

@github-actions github-actions bot locked and limited conversation to collaborators Jan 20, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants