Skip to content

Commit

Permalink
feat: container entity panel (#205)
Browse files Browse the repository at this point in the history
* feat: container entity panel

* docs: add 404 to container entity
  • Loading branch information
JonasBK authored Nov 14, 2023
1 parent 69df0e6 commit cfe845a
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 2 deletions.
4 changes: 4 additions & 0 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ func NewV2API(cfg config.Configuration, resources v2.Resources, routerInst *rout
routerInst.GET(fmt.Sprintf("/api/v2/computers/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/computers/{%s}/controllables", api.URIPathVariableObjectID), resources.ListADEntityControllables).RequirePermissions(permissions.GraphDBRead),

// Container Entity API
routerInst.GET(fmt.Sprintf("/api/v2/containers/{%s}", api.URIPathVariableObjectID), resources.GetContainerEntityInfo).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/containers/{%s}/controllers", api.URIPathVariableObjectID), resources.ListADEntityControllers).RequirePermissions(permissions.GraphDBRead),

// Domain Entity API
routerInst.PATCH(fmt.Sprintf("/api/v2/domains/{%s}", api.URIPathVariableObjectID), resources.PatchDomain).RequirePermissions(permissions.GraphDBRead),
routerInst.GET(fmt.Sprintf("/api/v2/domains/{%s}", api.URIPathVariableObjectID), resources.GetDomainEntityInfo).RequirePermissions(permissions.GraphDBRead),
Expand Down
51 changes: 51 additions & 0 deletions cmd/api/src/api/v2/container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2023 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package v2

import (
"fmt"
"net/http"

"github.com/specterops/bloodhound/src/api"
adAnalysis "github.com/specterops/bloodhound/analysis/ad"
"github.com/specterops/bloodhound/dawgs/graph"
"github.com/specterops/bloodhound/graphschema/ad"
)

var containerQueries = map[string]any{
"controllers": adAnalysis.FetchInboundADEntityControllers,
}

func (s *Resources) GetContainerEntityInfo(response http.ResponseWriter, request *http.Request) {
if hydrateCounts, err := api.ParseOptionalBool(request.URL.Query().Get(api.QueryParameterHydrateCounts), true); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsBadQueryParameterFilters, request), response)
} else if objectId, err := GetEntityObjectIDFromRequestPath(request); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("error reading objectid: %v", err), request), response)
} else if node, err := s.GraphQuery.GetEntityByObjectId(request.Context(), objectId, ad.Container); err != nil {
if graph.IsErrNotFound(err) {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, "node not found", request), response)
} else {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("error getting node: %v", err), request), response)
}
} else if hydrateCounts {
results := s.GraphQuery.GetEntityCountResults(request.Context(), node, containerQueries)
api.WriteBasicResponse(request.Context(), results, http.StatusOK, response)
} else {
results := map[string]any{"props": node.Properties.Map}
api.WriteBasicResponse(request.Context(), results, http.StatusOK, response)
}
}
110 changes: 110 additions & 0 deletions cmd/api/src/docs/json/paths/v2/container-entity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"/api/v2/containers/{object_id}": {
"parameters": [
{
"type": "string",
"description": "Container Object ID",
"name": "object_id",
"in": "path",
"required": true
}
],
"get": {
"description": "Get basic info and counts for this Container",
"tags": [
"Container Entity API",
"Community",
"Enterprise"
],
"summary": "Get container entity info",
"parameters": [
{
"$ref": "#/definitions/parameters.PreferHeader"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.BasicResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorWrapper"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.ErrorWrapper"
}
},
"504": {
"description": "Gateway Timeout",
"schema": {
"$ref": "#/definitions/api.ErrorWrapper"
}
}
}
}
},
"/api/v2/containers/{object_id}/controllers": {
"parameters": [
{
"type": "string",
"description": "Container Object ID",
"name": "object_id",
"in": "path",
"required": true
}
],
"get": {
"description": "List the principals that can control this Container through ACLs",
"tags": [
"Container Entity API",
"Community",
"Enterprise"
],
"summary": "List container controllers",
"parameters": [
{
"$ref": "#/definitions/parameters.PreferHeader"
},
{
"type": "integer",
"description": "Paging Skip",
"name": "skip",
"in": "query"
},
{
"type": "integer",
"description": "Paging Limit",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.ResponseWrapper"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.ErrorWrapper"
}
},
"504": {
"description": "Gateway Timeout",
"schema": {
"$ref": "#/definitions/api.ErrorWrapper"
}
}
}
}
}
}
8 changes: 8 additions & 0 deletions cmd/ui/src/ducks/entityinfo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export interface ComputerInfo extends EntityInfo {
sqlAdminUsers: number;
}

// --- Containers
export interface ContainersInfo extends EntityInfo {
props: BasicInfo & {
description?: string;
};
controllers: number;
}

// --- Domain
export interface DomainInfoGraph extends GraphInfo {
functionallevel: string;
Expand Down
1 change: 1 addition & 0 deletions cmd/ui/src/ducks/explore/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export enum GraphNodeTypes {
GPO = 'GPO',
OU = 'OU',
Domain = 'Domain',
Container = 'Container',
Meta = 'Meta',
}

Expand Down
14 changes: 12 additions & 2 deletions cmd/ui/src/views/Explore/EntityInfo/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ export const entityInformationEndpoints: Record<
[ActiveDirectoryNodeKind.Entity]: (id: string, options?: RequestOptions) => apiClient.getBaseV2(id, false, options),
[ActiveDirectoryNodeKind.Computer]: (id: string, options?: RequestOptions) =>
apiClient.getComputerV2(id, false, options),
[ActiveDirectoryNodeKind.Container]: () => Promise.resolve(),
[ActiveDirectoryNodeKind.Container]: (id: string, options?: RequestOptions) =>
apiClient.getContainerV2(id, false, options),
[ActiveDirectoryNodeKind.Domain]: (id: string, options?: RequestOptions) =>
apiClient.getDomainV2(id, false, options),
[ActiveDirectoryNodeKind.GPO]: (id: string, options?: RequestOptions) => apiClient.getGPOV2(id, false, options),
Expand Down Expand Up @@ -1418,6 +1419,16 @@ export const allSections: Record<GraphNodeTypes, (id: string) => EntityInfoDataT
.then((res) => res.data),
},
],
[ActiveDirectoryNodeKind.Container]: (id) => [
{
id,
label: 'Inbound Object Control',
endpoint: ({ skip, limit, type }) =>
apiClient
.getContainerControllersV2(id, skip, limit, type, { signal: controller.signal })
.then((res) => res.data),
},
],
[ActiveDirectoryNodeKind.Computer]: (id) => [
{
id,
Expand Down Expand Up @@ -1546,7 +1557,6 @@ export const allSections: Record<GraphNodeTypes, (id: string) => EntityInfoDataT
.then((res) => res.data),
},
],
[ActiveDirectoryNodeKind.Container]: () => [],
[ActiveDirectoryNodeKind.Domain]: (id) => [
{
id,
Expand Down
34 changes: 34 additions & 0 deletions packages/javascript/js-client-library/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,40 @@ class BHEAPIClient {
)
);

getContainerV2 = (id: string, counts?: boolean, options?: types.RequestOptions) =>
this.baseClient.get(
`/api/v2/containers/${id}`,
Object.assign(
{
params: {
counts,
},
},
options
)
);

getContainerControllersV2 = (
id: string,
skip?: number,
limit?: number,
type?: string,
options?: types.RequestOptions
) =>
this.baseClient.get(
`/api/v2/containers/${id}/controllers`,
Object.assign(
{
params: {
skip,
limit,
type,
},
},
options
)
);

getMetaV2 = (id: string, options?: types.RequestOptions) => this.baseClient.get(`/api/v2/meta/${id}`, options);

getShortestPathV2 = (
Expand Down

0 comments on commit cfe845a

Please sign in to comment.