Skip to content

Commit

Permalink
Owned object visual representation (#795)
Browse files Browse the repository at this point in the history
* WIP: Visual representation of owned object

* Visual confirmation of object during cypher queries.

* Removed unused lib from prev. testing

* Removed console log from prev. testing

* Removed unused icon and tag

* Removed redundant code

* Removed redundant code

* Code refactor

* Removed log message from testing, Code refactor

* Formating

* Convert tags back to string

* Changed to use defined constant for "owned"

* Code refactoring and handling of multiple spaces

* Code refactoring

 * Moved from using regex to utilize splice

* Changed tags to type const
  • Loading branch information
Palt authored Aug 21, 2024
1 parent b5aff8b commit 989ddae
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 21 deletions.
14 changes: 8 additions & 6 deletions cmd/api/src/model/unified_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ func NewUnifiedGraph() UnifiedGraph {

// UnifiedNode represents a single node in a graph containing a minimal set of attributes for graph rendering
type UnifiedNode struct {
Label string `json:"label"`
Kind string `json:"kind"`
ObjectId string `json:"objectId"`
IsTierZero bool `json:"isTierZero"`
LastSeen time.Time `json:"lastSeen"`
Properties map[string]any `json:"properties,omitempty"`
Label string `json:"label"`
Kind string `json:"kind"`
ObjectId string `json:"objectId"`
IsTierZero bool `json:"isTierZero"`
IsOwnedObject bool `json:"isOwnedObject"`
LastSeen time.Time `json:"lastSeen"`
Properties map[string]any `json:"properties,omitempty"`
}

// UnifiedEdge represents a single path segment in a graph containing a minimal set of attributes for graph rendering
Expand Down Expand Up @@ -78,6 +79,7 @@ func FromDAWGSNode(node *graph.Node, includeProperties bool) UnifiedNode {
Kind: analysis.GetNodeKind(node).String(),
ObjectId: objectId,
IsTierZero: strings.Contains(systemTags, ad.AdminTierZero),
IsOwnedObject: strings.Contains(systemTags, OwnedAssetGroupTag),
LastSeen: lastSeen,
Properties: properties,
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/ui/src/ducks/explore/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,10 @@ export const toggleTierZeroNode = (nodeId: string): types.GraphActionTypes => {
nodeId,
};
};

export const toggleOwnedObjectNode = (nodeId: string): types.GraphActionTypes => {
return {
type: types.TOGGLE_OWNED_OBJECT_NODE,
nodeId,
};
};
31 changes: 26 additions & 5 deletions cmd/ui/src/ducks/explore/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//
// SPDX-License-Identifier: Apache-2.0

import { OWNED_OBJECT_TAG, TIER_ZERO_TAG } from 'bh-shared-ui';
import { produce } from 'immer';
import * as types from 'src/ducks/explore/types';

Expand Down Expand Up @@ -49,12 +50,32 @@ const graphDataReducer = (state = initialGraphDataState, action: types.GraphActi
} else if (action.type === types.SAVE_RESPONSE_FOR_EXPORT) {
draft.export = action.payload;
} else if (action.type === types.TOGGLE_TIER_ZERO_NODE) {
const systemTags = state.chartProps.items[action.nodeId].data.system_tags;
// remove the tier zero tag from the node
if (systemTags === 'admin_tier_0') {
draft.chartProps.items[action.nodeId].data.system_tags = '';
let systemTags = []
// Check if system_tags contains tags then split, else leave empty
{ state.chartProps.items[action.nodeId].data.system_tags ?
systemTags = state.chartProps.items[action.nodeId].data.system_tags.split(" ") : null }
if (systemTags.includes(TIER_ZERO_TAG)) {
// Remove tag
systemTags.splice(systemTags.indexOf(TIER_ZERO_TAG), 1);
draft.chartProps.items[action.nodeId].data.system_tags = systemTags.join(' ');
} else {
draft.chartProps.items[action.nodeId].data.system_tags = 'admin_tier_0';
// Add tag
systemTags.push(TIER_ZERO_TAG);
draft.chartProps.items[action.nodeId].data.system_tags = systemTags.join(' ');
}
} else if (action.type === types.TOGGLE_OWNED_OBJECT_NODE) {
let systemTags = []
// Check if system_tags contains tags then split, else leave empty
{ state.chartProps.items[action.nodeId].data.system_tags ?
systemTags = state.chartProps.items[action.nodeId].data.system_tags.split(" ") : null }
if (systemTags.includes(OWNED_OBJECT_TAG)) {
// Remove tag
systemTags.splice(systemTags.indexOf(OWNED_OBJECT_TAG), 1);
draft.chartProps.items[action.nodeId].data.system_tags = systemTags.join(' ');
} else {
// Add tag
systemTags.push(OWNED_OBJECT_TAG);
draft.chartProps.items[action.nodeId].data.system_tags = systemTags.join(' ');
}
}
return draft;
Expand Down
10 changes: 9 additions & 1 deletion cmd/ui/src/ducks/explore/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const REMOVE_NODES = 'app/explore/REMOVENODE';
const SAVE_RESPONSE_FOR_EXPORT = 'app/explore/SAVE_RESPONSE_FOR_EXPORT';

const TOGGLE_TIER_ZERO_NODE = 'app/explore/TOGGLE_TIER_ZERO_NODE';
const TOGGLE_OWNED_OBJECT_NODE = 'app/explore/TOGGLE_OWNED_OBJECT_NODE';

export {
SET_GRAPH_LOADING,
Expand All @@ -41,6 +42,7 @@ export {
GRAPH_INIT,
SAVE_RESPONSE_FOR_EXPORT,
TOGGLE_TIER_ZERO_NODE,
TOGGLE_OWNED_OBJECT_NODE,
};

export enum GraphEndpoints {}
Expand Down Expand Up @@ -105,6 +107,11 @@ interface ToggleTierZeroNodeAction {
nodeId: string;
}

interface ToggleOwnedObjectNodeAction {
type: typeof TOGGLE_OWNED_OBJECT_NODE;
nodeId: string;
}

export type GraphActionTypes =
| SetGraphLoadingAction
| GraphStartAction
Expand All @@ -115,7 +122,8 @@ export type GraphActionTypes =
| RemoveNodeAction
| GraphInitAction
| SaveResponseForExportAction
| ToggleTierZeroNodeAction;
| ToggleTierZeroNodeAction
| ToggleOwnedObjectNodeAction;

export interface NodeInfoRequest {
type: typeof GRAPH_START;
Expand Down
8 changes: 7 additions & 1 deletion cmd/ui/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ type ThemedGlyph = {
color: string;
};
tierZeroGlyph: GlyphIconInfo;
ownedObjectGlyph: GlyphIconInfo;
};

export type ThemedOptions = {
Expand Down Expand Up @@ -176,6 +177,7 @@ export const transformFlatGraphResponse = (graph: FlatGraphResponse): GraphData
kind: node.data.nodetype,
objectId: node.data.objectid,
isTierZero: !!(node.data.system_tags && node.data.system_tags.indexOf('admin_tier_0') !== -1),
isOwnedObject: !!(node.data.system_tags && node.data.system_tags.indexOf('owned') !== -1),
lastSeen: lastSeen,
};
} else if (isLink(item)) {
Expand All @@ -201,6 +203,10 @@ export const transformToFlatGraphResponse = (graph: GraphResponse) => {
const result: any = {};
for (const [key, value] of Object.entries(graph.data.nodes)) {
const lastSeen = getLastSeenValue(value);
// Check and add needed system_tags to node
const tags = []
{ value.isTierZero ? tags.push('admin_tier_0') : null }
{ value.isOwnedObject? tags.push('owned') : null }
result[key] = {
label: {
text: value.label,
Expand All @@ -209,7 +215,7 @@ export const transformToFlatGraphResponse = (graph: GraphResponse) => {
nodetype: value.kind,
name: value.label,
objectid: value.objectId,
system_tags: value.isTierZero ? 'admin_tier_0' : undefined,
system_tags: tags.join(' '),
lastseen: lastSeen,
},
};
Expand Down
2 changes: 2 additions & 0 deletions cmd/ui/src/views/Explore/BasicObjectInfoFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
NodeIcon,
searchbarActions,
TIER_ZERO_TAG,
OWNED_OBJECT_TAG,
} from 'bh-shared-ui';
import { useAppDispatch } from 'src/store';

Expand Down Expand Up @@ -71,6 +72,7 @@ export const BasicObjectInfoFields: React.FC<BasicObjectInfoFieldsProps> = (prop
return (
<>
{props.system_tags?.includes(TIER_ZERO_TAG) && <Field label='Tier Zero:' value={true} />}
{props.system_tags?.includes(OWNED_OBJECT_TAG) && <Field label='Owned Object:' value={true} />}
{props.displayname && <Field label='Display Name:' value={props.displayname} />}
<Field label='Object ID:' value={props.objectid} />
{props.service_principal_id &&
Expand Down
14 changes: 10 additions & 4 deletions cmd/ui/src/views/Explore/ContextMenu/AssetGroupMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { Dialog, DialogActions, DialogContent, DialogTitle, MenuItem } from '@mu
import { apiClient, useNotifications } from 'bh-shared-ui';
import { FC, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { selectTierZeroAssetGroupId } from 'src/ducks/assetgroups/reducer';
import { toggleTierZeroNode } from 'src/ducks/explore/actions';
import { selectTierZeroAssetGroupId, selectOwnedAssetGroupId } from 'src/ducks/assetgroups/reducer';
import { toggleTierZeroNode, toggleOwnedObjectNode } from 'src/ducks/explore/actions';
import { useAppDispatch, useAppSelector } from 'src/store';
import { Button } from '@bloodhoundenterprise/doodleui';

Expand All @@ -31,8 +31,10 @@ const AssetGroupMenuItem: FC<{ assetGroupId: number; assetGroupName: string }> =

const selectedNode = useAppSelector((state) => state.entityinfo.selectedNode);
const tierZeroAssetGroupId = useAppSelector(selectTierZeroAssetGroupId);
const ownedObjectAssetGroupId = useAppSelector(selectOwnedAssetGroupId);

const isMenuItemForTierZero = assetGroupId === tierZeroAssetGroupId;
const isMenuItemForOwnedObject = assetGroupId === ownedObjectAssetGroupId;

const mutation = useMutation({
mutationFn: ({ nodeId, action }: { nodeId: string; action: 'add' | 'remove' }) => {
Expand All @@ -45,8 +47,12 @@ const AssetGroupMenuItem: FC<{ assetGroupId: number; assetGroupName: string }> =
]);
},
onSuccess: () => {
if (selectedNode?.graphId && isMenuItemForTierZero) {
dispatch(toggleTierZeroNode(selectedNode.graphId));
if (selectedNode?.graphId) {
if(isMenuItemForTierZero) {
dispatch(toggleTierZeroNode(selectedNode.graphId));
} else if (isMenuItemForOwnedObject) {
dispatch(toggleOwnedObjectNode(selectedNode.graphId));
}
}

addNotification('Update successful.', 'AssetGroupUpdateSuccess');
Expand Down
16 changes: 12 additions & 4 deletions cmd/ui/src/views/Explore/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const initGraph = (graph: MultiDirectedGraph, items: GraphData, theme: Th
color: theme.palette.neutral.primary, //border
},
tierZeroGlyph: darkMode ? GLYPHS[GlyphKind.TIER_ZERO_DARK] : GLYPHS[GlyphKind.TIER_ZERO],
ownedObjectGlyph: darkMode ? GLYPHS[GlyphKind.OWNED_OBJECT_DARK] : GLYPHS[GlyphKind.OWNED_OBJECT],
},
};

Expand All @@ -60,17 +61,24 @@ const initGraphNodes = (graph: MultiDirectedGraph, nodes: GraphNodes, themedOpti
const icon = NODE_ICON[node.kind] || UNKNOWN_ICON;
nodeParams.color = icon.color;
nodeParams.image = icon.url || '';
nodeParams.glyphs = []

// Tier zero nodes should be marked with a gem glyph
if (node.isTierZero) {
nodeParams.type = 'glyphs';
nodeParams.glyphs = [
{
nodeParams.glyphs.push({
location: GlyphLocation.TOP_RIGHT,
image: themedOptions.glyph.tierZeroGlyph.url || '',
...themedOptions.glyph.colors,
},
];
});
}
if (node.isOwnedObject) {
nodeParams.type = 'glyphs';
nodeParams.glyphs.push({
location: GlyphLocation.BOTTOM_RIGHT,
image: themedOptions.glyph.ownedObjectGlyph.url || '',
...themedOptions.glyph.colors,
});
}

graph.addNode(key, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,23 @@ const nodes: GraphNodes = {
kind: 'Computer',
objectId: '001',
isTierZero: false,
isOwnedObject: false,
lastSeen: '',
},
'2': {
label: 'user_node',
kind: 'User',
objectId: '002',
isTierZero: false,
isOwnedObject: false,
lastSeen: '',
},
'3': {
label: 'group_node',
kind: 'Group',
objectId: '003',
isTierZero: false,
isOwnedObject: false,
lastSeen: '',
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type GraphNode = {
objectId: string;
lastSeen: string;
isTierZero: boolean;
isOwnedObject: boolean;
descendent_count?: number | null;
};

Expand Down
1 change: 1 addition & 0 deletions packages/javascript/bh-shared-ui/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const ZERO_VALUE_API_DATE = '0001-01-01T00:00:00Z';

export const TIER_ZERO_TAG = 'admin_tier_0';
export const TIER_ZERO_LABEL = 'High Value';
export const OWNED_OBJECT_TAG = 'owned';

export const lightPalette = {
primary: {
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/bh-shared-ui/src/utils/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
faArrowsLeftRightToLine,
faBuilding,
faClipboardCheck,
faSkull,
} from '@fortawesome/free-solid-svg-icons';
import { ActiveDirectoryNodeKind, AzureNodeKind } from '../graphSchema';

Expand All @@ -68,6 +69,8 @@ export type GlyphDictionary = {
export enum GlyphKind {
TIER_ZERO,
TIER_ZERO_DARK,
OWNED_OBJECT,
OWNED_OBJECT_DARK,
EXPAND,
COLLAPSE,
}
Expand Down Expand Up @@ -243,6 +246,16 @@ export const GLYPHS: GlyphDictionary = {
color: '#FFFFFF',
iconColor: '#000000',
},
[GlyphKind.OWNED_OBJECT]: {
icon: faSkull,
color: '#000000',
iconColor: '#FFFFFF',
},
[GlyphKind.OWNED_OBJECT_DARK]: {
icon: faSkull,
color: '#FFFFFF',
iconColor: '#000000',
},
[GlyphKind.EXPAND]: {
icon: faPlus,
color: '#FFFFFF',
Expand Down
1 change: 1 addition & 0 deletions packages/javascript/js-client-library/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export type GraphNode = {
objectId: string;
lastSeen: string;
isTierZero: boolean;
isOwnedObject: boolean;
descendent_count?: number | null;
};

Expand Down

0 comments on commit 989ddae

Please sign in to comment.