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, display-name mappings, and additional metadata (collected status, impact, hygiene attack paths, exposures).
    • New feature flag "OpenGraph Findings" added to control OpenGraph-based environment discovery and filtering.
  • Refactor

    • Renamed domain-focused terminology and UI-facing endpoints to environment-centric names for consistency.
  • 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

This pull request refactors domain handling to support generic environments. It renames DomainSelector/DomainSelectors to EnvironmentSelector/EnvironmentSelectors, adds new fields (Collected, ImpactValue, HygieneAttackPaths, Exposures), enhances the /api/v2/available-domains endpoint with database-driven filtering, adds FeatureOpenGraphFindings flag, and extends the SchemaEnvironment model with display name fields.

Changes

Cohort / File(s) Summary
Environment Selector Type System
cmd/api/src/model/search.go, cmd/api/src/model/search_test.go, cmd/api/src/model/appcfg/flag.go
Renamed DomainSelector/DomainSelectors to EnvironmentSelector/EnvironmentSelectors with expanded struct fields (Collected, ImpactValue, HygieneAttackPaths, Exposures). Updated GetFilterCriteria signature to accept []graph.Kind parameter. Added FeatureOpenGraphFindings constant.
API Handler Implementation
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
Renamed GetAvailableDomains handler to ListAvailableEnvironments. Added BuildEnvironmentFilter, BuildEnvironmentSelectors functions with context initialization, sort parameter parsing, and error handling. Introduced resolveEnvType and resolveCollected helpers for environment type resolution. Updated test cases with mock DB interactions and environment-driven filtering.
Database Layer & Schema Extensions
cmd/api/src/database/graphschema.go, cmd/api/src/database/graphschema_integration_test.go, cmd/api/src/model/graphschema.go
Refactored GetEnvironments to use explicit SQL query with joins for populating display names (SchemaExtensionDisplayName, EnvironmentKindName). Added new public fields to SchemaEnvironment struct for display name mapping. Updated integration tests to verify new fields.
Database Migration & Configuration
cmd/api/src/database/migration/migrations/v8.6.0.sql, cmd/api/src/config/config.go, cmd/api/src/config/default.go
Added opengraph_findings feature flag insertion to v8.6.0 migration. Minor whitespace formatting adjustments to configuration struct initialization.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant APIHandler as ListAvailableEnvironments<br/>Handler
    participant DB as Database
    participant Graph as Graph Engine
    
    Client->>APIHandler: GET /api/v2/available-environments
    APIHandler->>DB: GetEnvironments(ctx)
    DB-->>APIHandler: []SchemaEnvironment
    APIHandler->>DB: GetFlagByKey(ctx, "opengraph_findings")
    DB-->>APIHandler: FeatureFlag
    APIHandler->>APIHandler: BuildEnvironmentFilter(ctx, db, request)
    APIHandler->>APIHandler: Parse sort parameters with EnvironmentSelectors
    APIHandler->>Graph: GraphQuery(filter criteria)
    Graph-->>APIHandler: []*graph.Node
    APIHandler->>APIHandler: BuildEnvironmentSelectors(nodes, kindToDisplayName)
    APIHandler->>APIHandler: Resolve type & collected status per node
    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

  • LawsonWillard
  • AD7ZJ
  • urangel

Poem

🐰 With whiskers twitched and nose held high,
We've swapped domains for environments spry!
From selectors old to kinds that play,
The schema dances a better way. 🌿✨

🚥 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: updating the available domains endpoint to support OpenGraph environments, with a reference to the ticket BED-6761.
Description check ✅ Passed The PR description includes all major required sections: Description, Motivation and Context with issue reference (BED-6761), and How Has This Been Tested. The description clearly explains the changes and their purpose.

✏️ 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants