diff --git a/cmd/api/src/docs/json/definitions/models.json b/cmd/api/src/docs/json/definitions/models.json index 78b308b5a8..0821c4b274 100644 --- a/cmd/api/src/docs/json/definitions/models.json +++ b/cmd/api/src/docs/json/definitions/models.json @@ -65,7 +65,10 @@ "properties": { "action": { "type": "string", - "enum": ["add", "remove"] + "enum": [ + "add", + "remove" + ] }, "selector_name": { "type": "string" @@ -562,7 +565,12 @@ }, "model.PaginatedResponse": { "type": "object", - "required": ["count", "limit", "skip", "data"], + "required": [ + "count", + "limit", + "skip", + "data" + ], "properties": { "count": { "type": "integer" @@ -1105,5 +1113,25 @@ "format": "date-time" } } + }, + "model.SearchResult": { + "type": "object", + "properties": { + "objectid": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "distinguishedname": { + "type": "string" + }, + "system_tags": { + "type": "string" + } + } } -} +} \ No newline at end of file diff --git a/cmd/api/src/docs/json/definitions/v2.json b/cmd/api/src/docs/json/definitions/v2.json index 39a19512e3..f6d86b069d 100644 --- a/cmd/api/src/docs/json/definitions/v2.json +++ b/cmd/api/src/docs/json/definitions/v2.json @@ -28,7 +28,10 @@ }, "v2.RiskAcceptRequest": { "type": "object", - "required": ["risk_type", "accepted"], + "required": [ + "risk_type", + "accepted" + ], "properties": { "risk_type": { "type": "string" @@ -57,9 +60,15 @@ "data": { "type": "object", "properties": { - "count": { "type": "integer" }, - "limit": { "type": "integer" }, - "skip": { "type": "integer" }, + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "skip": { + "type": "integer" + }, "members": { "type": "array", "items": { @@ -93,7 +102,10 @@ }, "v2.ClientCreateRequest": { "type": "object", - "required": ["name", "type"], + "required": [ + "name", + "type" + ], "properties": { "domain_controller": { "type": "string" @@ -111,7 +123,9 @@ }, "v2.ClientUpdateRequest": { "type": "object", - "required": ["name"], + "required": [ + "name" + ], "properties": { "name": { "type": "string" @@ -222,7 +236,10 @@ "status": { "description": "Status code for complete (2) or failed (5)", "type": "integer", - "enum": [2, 5], + "enum": [ + 2, + 5 + ], "required": true }, "message": { @@ -477,7 +494,10 @@ }, "v2.CreateUserRequest": { "type": "object", - "required": ["email_address", "roles"], + "required": [ + "email_address", + "roles" + ], "properties": { "email_address": { "type": "string", @@ -618,7 +638,9 @@ }, "v2.ListAppConfigParametersResponse": { "type": "object", - "required": ["data"], + "required": [ + "data" + ], "properties": { "data": { "type": "object" @@ -627,7 +649,9 @@ }, "v2.ListFlagsResponse": { "type": "object", - "required": ["data"], + "required": [ + "data" + ], "properties": { "data": { "type": "object" @@ -636,7 +660,9 @@ }, "v2.ToggleFlagResponse": { "type": "object", - "required": ["enabled"], + "required": [ + "enabled" + ], "properties": { "enabled": { "type": "boolean" @@ -645,7 +671,9 @@ }, "v2.ListSAMLSignOnEndpointsResponse": { "type": "object", - "required": ["endpoints"], + "required": [ + "endpoints" + ], "properties": { "endpoints": { "type": "boolean" @@ -654,7 +682,9 @@ }, "v2.IDPValidationResponse": { "type": "object", - "required": ["successful"], + "required": [ + "successful" + ], "properties": { "error_message": { "type": "string" @@ -697,7 +727,9 @@ }, "v2.MFAEnrollmentRequest": { "type": "object", - "required": ["secret"], + "required": [ + "secret" + ], "properties": { "secret": { "type": "string" @@ -717,7 +749,9 @@ }, "v2.MFAStatusResponse": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { "data": { "type": "object", @@ -731,7 +765,9 @@ }, "v2.MFAActivationRequest": { "type": "object", - "required": ["otp"], + "required": [ + "otp" + ], "properties": { "otp": { "type": "string" @@ -740,7 +776,9 @@ }, "v2.LoginResponse": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { "data": { "type": "object", @@ -760,7 +798,9 @@ }, "v2.ClientErrorRequest": { "type": "object", - "required": ["task_error"], + "required": [ + "task_error" + ], "properties": { "task_error": { "type": "string" @@ -772,7 +812,9 @@ }, "v2.PagedNodeListEntry": { "type": "object", - "required": ["object_id"], + "required": [ + "object_id" + ], "properties": { "object_id": { "type": "string" @@ -920,7 +962,10 @@ }, "v2.CreateSavedQueryRequest": { "type": "object", - "required": ["name", "query"], + "required": [ + "name", + "query" + ], "properties": { "query": { "type": "string" @@ -929,5 +974,16 @@ "type": "string" } } + }, + "v2.SearchResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/model.SearchResult" + } + } + } } -} +} \ No newline at end of file diff --git a/cmd/api/src/docs/json/paths/v2/search.json b/cmd/api/src/docs/json/paths/v2/search.json index 3413a707eb..a3b068132d 100644 --- a/cmd/api/src/docs/json/paths/v2/search.json +++ b/cmd/api/src/docs/json/paths/v2/search.json @@ -102,7 +102,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/definitions/api.ResponseWrapper" + "$ref": "#/definitions/v2.SearchResponse" } } } @@ -113,4 +113,4 @@ } } } -} +} \ No newline at end of file diff --git a/cmd/api/src/model/model.go b/cmd/api/src/model/model.go index 26c4ac585a..c6450e08c8 100644 --- a/cmd/api/src/model/model.go +++ b/cmd/api/src/model/model.go @@ -1,17 +1,17 @@ // 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 model @@ -97,4 +97,5 @@ type SearchResult struct { Type string `json:"type"` Name string `json:"name"` DistinguishedName string `json:"distinguishedname"` + SystemTags string `json:"system_tags"` } diff --git a/cmd/api/src/queries/graph.go b/cmd/api/src/queries/graph.go index f3932c2134..d16348b855 100644 --- a/cmd/api/src/queries/graph.go +++ b/cmd/api/src/queries/graph.go @@ -448,6 +448,7 @@ func nodeToSearchResult(node *graph.Node) model.SearchResult { name, _ = node.Properties.GetOrDefault(common.Name.String(), "NO NAME").String() objectID, _ = node.Properties.GetOrDefault(common.ObjectID.String(), "NO OBJECT ID").String() distinguishedName, _ = node.Properties.GetOrDefault(ad.DistinguishedName.String(), "").String() + systemTags, _ = node.Properties.GetOrDefault(common.SystemTags.String(), "").String() ) return model.SearchResult{ @@ -455,6 +456,7 @@ func nodeToSearchResult(node *graph.Node) model.SearchResult { Type: analysis.GetNodeKindDisplayLabel(node), Name: name, DistinguishedName: distinguishedName, + SystemTags: systemTags, } } diff --git a/cmd/ui/src/components/Header.tsx b/cmd/ui/src/components/Header.tsx index 38e262f6f3..fffa7bd6f4 100644 --- a/cmd/ui/src/components/Header.tsx +++ b/cmd/ui/src/components/Header.tsx @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -import { faCog, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; +import { faCog, faUsersRectangle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { AppBar, Box, IconButton, Link, Toolbar } from '@mui/material'; import makeStyles from '@mui/styles/makeStyles'; @@ -94,6 +94,13 @@ const Header: React.FC = () => { onClick={() => navigate(routes.ROUTE_EXPLORE)} data-testid='global_header_nav-explore' /> + } + active={location.pathname === routes.ROUTE_GROUP_MANAGEMENT} + onClick={() => navigate(routes.ROUTE_GROUP_MANAGEMENT)} + data-testid='global_header_nav-group-management' + />
diff --git a/cmd/ui/src/constants.ts b/cmd/ui/src/constants.ts index 11fe1ed03c..5f773bd21a 100644 --- a/cmd/ui/src/constants.ts +++ b/cmd/ui/src/constants.ts @@ -23,3 +23,4 @@ export const MODERATE_THRESHOLD = 40; 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'; diff --git a/cmd/ui/src/ducks/global/routes.ts b/cmd/ui/src/ducks/global/routes.ts index eb1c38ca56..7ab22eeac1 100644 --- a/cmd/ui/src/ducks/global/routes.ts +++ b/cmd/ui/src/ducks/global/routes.ts @@ -16,6 +16,7 @@ export const ROUTE_HOME = '/'; export const ROUTE_EXPLORE = '/explore'; +export const ROUTE_GROUP_MANAGEMENT = '/group-management'; export const ROUTE_LOGIN = '/login'; export const ROUTE_CHANGE_PASSWORD = '/changepassword'; export const ROUTE_USER_DISABLED = '/user-disabled'; diff --git a/cmd/ui/src/mocks/factories.ts b/cmd/ui/src/mocks/factories.ts index 471d4394fa..2944ab0b27 100644 --- a/cmd/ui/src/mocks/factories.ts +++ b/cmd/ui/src/mocks/factories.ts @@ -80,3 +80,11 @@ export const createMockSearchResult = (nodeType?: string) => { ]), }; }; + +export const createMockDomain = () => ({ + type: faker.helpers.arrayElement(['active-directory', 'azure']), + impactValue: faker.datatype.number({ min: 0, max: 100 }), + name: faker.internet.domainName(), + id: faker.datatype.uuid(), + collected: faker.datatype.boolean(), +}); diff --git a/cmd/ui/src/utils.ts b/cmd/ui/src/utils.ts index b1f594107f..363e5d6e6f 100644 --- a/cmd/ui/src/utils.ts +++ b/cmd/ui/src/utils.ts @@ -14,7 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 -import { ActiveDirectoryNodeKind, AzureNodeKind, apiClient } from 'bh-shared-ui'; +import { apiClient } from 'bh-shared-ui'; import { FlatGraphResponse, GraphData, GraphResponse, StyledGraphEdge, StyledGraphNode } from 'js-client-library'; import identity from 'lodash/identity'; import throttle from 'lodash/throttle'; @@ -40,21 +40,6 @@ export const getDatesInRange = (startDate: Date, endDate: Date) => { return dates; }; -export const validateNodeType = (type: string): ActiveDirectoryNodeKind | AzureNodeKind | undefined => { - let result = undefined; - Object.values(ActiveDirectoryNodeKind).forEach((activeDirectoryType: string) => { - if (activeDirectoryType.localeCompare(type, undefined, { sensitivity: 'base' }) === 0) - result = activeDirectoryType as ActiveDirectoryNodeKind; - }); - - Object.values(AzureNodeKind).forEach((azureType: string) => { - if (azureType.localeCompare(type, undefined, { sensitivity: 'base' }) === 0) - result = azureType as AzureNodeKind; - }); - - return result; -}; - export const getUsername = (user: any): string | undefined => { if (user?.first_name && user?.last_name) { return `${user.first_name} ${user.last_name}`; diff --git a/cmd/ui/src/views/Content.tsx b/cmd/ui/src/views/Content.tsx index a652967a26..cd8eb90a75 100644 --- a/cmd/ui/src/views/Content.tsx +++ b/cmd/ui/src/views/Content.tsx @@ -38,6 +38,7 @@ const UserProfile = React.lazy(() => import('bh-shared-ui').then((module) => ({ const DownloadCollectors = React.lazy(() => import('./DownloadCollectors')); const Administration = React.lazy(() => import('./Administration')); const ApiExplorer = React.lazy(() => import('./ApiExplorer')); +const GroupManagement = React.lazy(() => import('./GroupManagement/GroupManagement')); const useStyles = makeStyles({ content: { @@ -112,6 +113,11 @@ const Content: React.FC = () => { component: ExploreGraphView, authenticationRequired: true, }, + { + path: routes.ROUTE_GROUP_MANAGEMENT, + component: GroupManagement, + authenticationRequired: true, + }, { path: routes.ROUTE_MY_PROFILE, component: UserProfile, diff --git a/cmd/ui/src/views/Explore/BasicObjectInfoFields.tsx b/cmd/ui/src/views/Explore/BasicObjectInfoFields.tsx new file mode 100644 index 0000000000..30d0cf7006 --- /dev/null +++ b/cmd/ui/src/views/Explore/BasicObjectInfoFields.tsx @@ -0,0 +1,85 @@ +// 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 + +import { Box } from '@mui/material'; +import { NodeIcon, Field, AzureNodeKind, EntityKinds } from 'bh-shared-ui'; +import { TIER_ZERO_TAG } from 'src/constants'; +import { sourceNodeSelected } from 'src/ducks/searchbar/actions'; +import { useAppDispatch } from 'src/store'; + +interface BasicObjectInfoFieldsProps { + objectid: string; + displayname?: string; + system_tags?: string; + service_principal_id?: string; + noderesourcegroupid?: string; + name?: string; +} + +const RelatedKindField = (fieldLabel: string, relatedKind: EntityKinds, id: string, name?: string) => { + const dispatch = useAppDispatch(); + return ( + + + {fieldLabel} + +
+ + + { + dispatch( + sourceNodeSelected({ + objectid: id, + type: relatedKind, + name: name || '', + }) + ); + }} + style={{ cursor: 'pointer' }} + overflow='hidden' + textOverflow='ellipsis' + title={id}> + {id} + + +
+ ); +}; + +export const BasicObjectInfoFields: React.FC = (props): JSX.Element => { + return ( + <> + {props.system_tags?.includes(TIER_ZERO_TAG) && } + {props.displayname && } + + {props.service_principal_id && + RelatedKindField( + 'Service Principal ID:', + AzureNodeKind.ServicePrincipal, + props.service_principal_id, + props.name + )} + {props.noderesourcegroupid && + RelatedKindField( + 'Node Resource Group ID:', + AzureNodeKind.ResourceGroup, + props.noderesourcegroupid, + props.name + )} + + ); +}; diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoCollapsibleSection.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoCollapsibleSection.tsx index a9f0008bb6..7cc03e3db3 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoCollapsibleSection.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoCollapsibleSection.tsx @@ -17,12 +17,10 @@ import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Accordion, AccordionDetails, AccordionSummary } from '@mui/material'; -import { EdgeInfoState, EdgeSections, edgeSectionToggle } from 'bh-shared-ui'; +import { EdgeInfoState, EdgeSections, edgeSectionToggle, SubHeader, useCollapsibleSectionStyles } from 'bh-shared-ui'; import React, { PropsWithChildren } from 'react'; import { useSelector } from 'react-redux'; import { AppState, useAppDispatch } from 'src/store'; -import useCollapsibleSectionStyles from 'src/views/Explore/InfoStyles/CollapsibleSection'; -import { SubHeader } from 'src/views/Explore/fragments'; export const EdgeInfoCollapsibleSection: React.FC< PropsWithChildren<{ diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoHeader.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoHeader.tsx index a9a064bb99..90b4b5032a 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoHeader.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoHeader.tsx @@ -20,7 +20,7 @@ import { Typography } from '@mui/material'; import { Icon, collapseAllSections } from 'bh-shared-ui'; import React from 'react'; import { useAppDispatch } from 'src/store'; -import useHeaderStyles from 'src/views/Explore/InfoStyles/Header'; +import { useHeaderStyles } from 'bh-shared-ui'; interface HeaderProps { name: string; diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoPane.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoPane.tsx index eea69c18f9..c2d643f066 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoPane.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeInfoPane.tsx @@ -14,19 +14,19 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Paper } from '@mui/material'; +import { Box, Paper, SxProps } from '@mui/material'; import { SelectedEdge } from 'bh-shared-ui'; import React, { useState } from 'react'; import EdgeInfoContent from 'src/views/Explore/EdgeInfo/EdgeInfoContent'; import Header from 'src/views/Explore/EdgeInfo/EdgeInfoHeader'; -import usePaneStyles from 'src/views/Explore/InfoStyles/Pane'; +import { usePaneStyles } from 'bh-shared-ui'; -const EdgeInfoPane: React.FC<{ selectedEdge: SelectedEdge }> = ({ selectedEdge }) => { +const EdgeInfoPane: React.FC<{ selectedEdge: SelectedEdge; sx?: SxProps }> = ({ selectedEdge, sx }) => { const styles = usePaneStyles(); const [expanded, setExpanded] = useState(true); return ( -
+
= ({ selectedEdge } }}> {selectedEdge === null ? 'No information to display.' : } -
+ ); }; diff --git a/cmd/ui/src/views/Explore/EdgeInfo/EdgeObjectInformation.tsx b/cmd/ui/src/views/Explore/EdgeInfo/EdgeObjectInformation.tsx index 41da185ff1..425375e999 100644 --- a/cmd/ui/src/views/Explore/EdgeInfo/EdgeObjectInformation.tsx +++ b/cmd/ui/src/views/Explore/EdgeInfo/EdgeObjectInformation.tsx @@ -15,11 +15,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Skeleton } from '@mui/material'; -import { EntityField, SelectedEdge, apiClient } from 'bh-shared-ui'; +import { SelectedEdge, apiClient, EntityField, FieldsContainer, ObjectInfoFields } from 'bh-shared-ui'; import { FC } from 'react'; import { useQuery } from 'react-query'; import EdgeInfoCollapsibleSection from 'src/views/Explore/EdgeInfo/EdgeInfoCollapsibleSection'; -import { FieldsContainer, ObjectInfoFields } from 'src/views/Explore/fragments'; import { formatObjectInfoFields } from 'src/views/Explore/utils'; const selectedEdgeCypherQuery = (sourceId: string | number, targetId: string | number, edgeKind: string): string => diff --git a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoCollapsibleSection.tsx b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoCollapsibleSection.tsx index 2e577f76d4..f8dc2620b6 100644 --- a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoCollapsibleSection.tsx +++ b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoCollapsibleSection.tsx @@ -19,8 +19,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Accordion, AccordionDetails, AccordionSummary, Alert, AlertTitle } from '@mui/material'; import React, { PropsWithChildren } from 'react'; import { useEntityInfoPanelContext } from './EntityInfoPanelContext'; -import { SubHeader } from 'src/views/Explore/fragments'; -import useCollapsibleSectionStyles from 'src/views/Explore/InfoStyles/CollapsibleSection'; +import { SubHeader, useCollapsibleSectionStyles } from 'bh-shared-ui'; const EntityInfoCollapsibleSectionError: React.FC<{ error: any }> = ({ error }) => { //TODO: Once azure backend changes for counts param are in, utilize response error details diff --git a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoHeader.tsx b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoHeader.tsx index 23b59cc2f8..13d9bfb947 100644 --- a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoHeader.tsx +++ b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoHeader.tsx @@ -20,7 +20,7 @@ import { Typography } from '@mui/material'; import { Icon, NodeIcon, EntityKinds } from 'bh-shared-ui'; import React from 'react'; import { useEntityInfoPanelContext } from 'src/views/Explore/EntityInfo/EntityInfoPanelContext'; -import useHeaderStyles from 'src/views/Explore/InfoStyles/Header'; +import { useHeaderStyles } from 'bh-shared-ui'; interface HeaderProps { name?: string; diff --git a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoPanel.tsx b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoPanel.tsx index f37342d70e..4148e2ac5e 100644 --- a/cmd/ui/src/views/Explore/EntityInfo/EntityInfoPanel.tsx +++ b/cmd/ui/src/views/Explore/EntityInfo/EntityInfoPanel.tsx @@ -14,21 +14,24 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Paper } from '@mui/material'; +import { Box, Paper, SxProps } from '@mui/material'; import React, { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import usePreviousValue from 'src/hooks/usePreviousValue'; -import { AppState } from 'src/store'; import EntityInfoContent from './EntityInfoContent'; import Header from './EntityInfoHeader'; import { EntityInfoPanelContextProvider } from './EntityInfoPanelContextProvider'; import { useEntityInfoPanelContext } from './EntityInfoPanelContext'; -import usePaneStyles from 'src/views/Explore/InfoStyles/Pane'; +import { usePaneStyles } from 'bh-shared-ui'; +import { SelectedNode } from 'src/ducks/entityinfo/types'; -const EntityInfoPanel: React.FC = () => { +interface EntityInfoPanelProps { + selectedNode: SelectedNode | null; + sx?: SxProps; +} + +const EntityInfoPanel: React.FC = ({ selectedNode, sx }) => { const styles = usePaneStyles(); const [expanded, setExpanded] = useState(true); - const selectedNode = useSelector((state: AppState) => state.entityinfo.selectedNode); const { setExpandedSections } = useEntityInfoPanelContext(); const previousSelectedNode = usePreviousValue(selectedNode); @@ -40,7 +43,7 @@ const EntityInfoPanel: React.FC = () => { if (selectedNode === null) { return ( -
+
{ }}> No information to display. -
+ ); } return ( -
+
{ style={{ display: expanded ? 'initial' : 'none', }}> - + -
+ ); }; -const WrappedEntityInfoPanel = () => ( +const WrappedEntityInfoPanel: React.FC = (props) => ( - + ); diff --git a/cmd/ui/src/views/Explore/EntityInfo/EntityObjectInformation.tsx b/cmd/ui/src/views/Explore/EntityInfo/EntityObjectInformation.tsx index 426012a048..ffacaa3f10 100644 --- a/cmd/ui/src/views/Explore/EntityInfo/EntityObjectInformation.tsx +++ b/cmd/ui/src/views/Explore/EntityInfo/EntityObjectInformation.tsx @@ -14,11 +14,11 @@ // // SPDX-License-Identifier: Apache-2.0 -import { EntityField } from 'bh-shared-ui'; import React from 'react'; -import EntityInfoCollapsibleSection from 'src/views/Explore/EntityInfo/EntityInfoCollapsibleSection'; -import { BasicObjectInfoFields, FieldsContainer, ObjectInfoFields } from 'src/views/Explore/fragments'; +import EntityInfoCollapsibleSection from './EntityInfoCollapsibleSection'; +import { EntityField, FieldsContainer, ObjectInfoFields } from 'bh-shared-ui'; import { formatObjectInfoFields } from 'src/views/Explore/utils'; +import { BasicObjectInfoFields } from '../BasicObjectInfoFields'; const EntityObjectInformation: React.FC<{ props: any }> = ({ props }) => { const formattedObjectFields: EntityField[] = formatObjectInfoFields(props); diff --git a/cmd/ui/src/views/Explore/ExploreSearchCombobox/ExploreSearchCombobox.tsx b/cmd/ui/src/views/Explore/ExploreSearchCombobox/ExploreSearchCombobox.tsx index c15243f6c7..6bb05abb6b 100644 --- a/cmd/ui/src/views/Explore/ExploreSearchCombobox/ExploreSearchCombobox.tsx +++ b/cmd/ui/src/views/Explore/ExploreSearchCombobox/ExploreSearchCombobox.tsx @@ -16,8 +16,14 @@ import { List, ListItem, ListItemText, Paper, TextField, useTheme } from '@mui/material'; import { useCombobox } from 'downshift'; -import { NodeIcon, SearchResultItem } from 'bh-shared-ui'; -import { getEmptyResultsText, getKeywordAndTypeValues, SearchResult, useSearch } from 'src/hooks/useSearch'; +import { + NodeIcon, + SearchResultItem, + getEmptyResultsText, + getKeywordAndTypeValues, + SearchResult, + useSearch, +} from 'bh-shared-ui'; import { SearchNodeType } from 'src/ducks/searchbar/types'; const ExploreSearchCombobox: React.FC<{ diff --git a/cmd/ui/src/views/Explore/GraphView.tsx b/cmd/ui/src/views/Explore/GraphView.tsx index d881478bee..e32194d42c 100644 --- a/cmd/ui/src/views/Explore/GraphView.tsx +++ b/cmd/ui/src/views/Explore/GraphView.tsx @@ -225,22 +225,36 @@ const GraphView: FC = () => { }; const GridItems = () => { + const selectedNode = useSelector((state: AppState) => state.entityinfo.selectedNode); + const columnsDefault = { xs: 6, md: 5, lg: 4, xl: 3 }; const cypherSearchColumns = { xs: 6, md: 6, lg: 6, xl: 4 }; const edgeInfoState: EdgeInfoState = useSelector((state: AppState) => state.edgeinfo); const [columns, setColumns] = useState(columnsDefault); + const theme = useTheme(); + + const columnStyles = { height: '100%' }; + + const infoPanelStyles = { + margin: theme.spacing(0, 4, 2, 2), + maxHeight: '95%', + }; const handleCypherTab = (isCypherEditorActive: boolean) => { isCypherEditorActive ? setColumns(cypherSearchColumns) : setColumns(columnsDefault); }; return [ - + , - - {edgeInfoState.open ? : } + + {edgeInfoState.open ? ( + + ) : ( + + )} , ]; }; diff --git a/cmd/ui/src/views/Explore/utils.ts b/cmd/ui/src/views/Explore/utils.ts index 1c20e0f09e..ca7c85e081 100644 --- a/cmd/ui/src/views/Explore/utils.ts +++ b/cmd/ui/src/views/Explore/utils.ts @@ -36,7 +36,7 @@ import { NODE_ICON, GLYPHS, UNKNOWN_ICON } from './svgIcons'; export const formatObjectInfoFields = (props: any): EntityField[] => { let mappedFields: EntityField[] = []; - const propKeys = Object.keys(props); + const propKeys = Object.keys(props || {}); for (let i = 0; i < propKeys.length; i++) { const value = props[propKeys[i]]; diff --git a/cmd/ui/src/views/GroupManagement/GroupManagement.test.tsx b/cmd/ui/src/views/GroupManagement/GroupManagement.test.tsx new file mode 100644 index 0000000000..97d9fd4735 --- /dev/null +++ b/cmd/ui/src/views/GroupManagement/GroupManagement.test.tsx @@ -0,0 +1,115 @@ +// 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 + +import { setupServer } from 'msw/node'; +import { act, render, waitFor } from '../../test-utils'; +import GroupManagement from './GroupManagement'; +import { rest } from 'msw'; +import { createMockDomain } from 'src/mocks/factories'; +import { createMockAssetGroup, createMockAssetGroupMembers } from 'bh-shared-ui'; +import userEvent from '@testing-library/user-event'; + +const domain = createMockDomain(); +const assetGroup = createMockAssetGroup(); +const assetGroupMembers = createMockAssetGroupMembers(); + +const server = setupServer( + rest.get('/api/v2/available-domains', (req, res, ctx) => { + return res(ctx.json({ data: [domain] })); + }), + rest.get('/api/v2/asset-groups', (req, res, ctx) => { + return res(ctx.json({ data: { asset_groups: [assetGroup] } })); + }), + rest.get('/api/v2/asset-groups/1/members', (req, res, ctx) => { + return res( + ctx.json({ + count: assetGroupMembers.members.length, + limit: 100, + skip: 0, + data: assetGroupMembers, + }) + ); + }), + rest.get('*', (req, res, ctx) => res(ctx.json({ data: [] }))) +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('GroupManagement', () => { + const setup = async () => + await act(async () => { + const user = userEvent.setup(); + const screen = render(); + return { user, screen }; + }); + + it('renders group and tenant dropdown selectors', async () => { + const { screen } = await setup(); + const groupSelector = screen.getByTestId('dropdown_context-selector'); + const tenantSelector = await waitFor(() => screen.getByTestId('data-quality_context-selector')); + + expect(screen.getByText('Group:')).toBeInTheDocument(); + expect(screen.getByText('Environment:')).toBeInTheDocument(); + expect(groupSelector).toBeInTheDocument(); + expect(tenantSelector).toBeInTheDocument(); + }); + + it('renders an edit form for the selected asset group', async () => { + const { screen } = await setup(); + const input = screen.getByRole('combobox'); + expect(input).toBeInTheDocument(); + }); + + it('renders a list of asset group members', async () => { + const { screen } = await setup(); + const member = assetGroupMembers.members[0]; + + expect(screen.getByRole('table')).toBeInTheDocument(); + expect(screen.getByText(member.name)).toBeInTheDocument(); + }); + + it('renders an empty message for the entity panel before a node is selected', async () => { + const { screen } = await setup(); + + expect(screen.getByText('None Selected')).toBeInTheDocument(); + expect(screen.getByText('No information to display.')).toBeInTheDocument(); + }); + + it('renders the node in the entity panel when member is clicked', async () => { + const { screen, user } = await setup(); + const member = assetGroupMembers.members[0]; + const listItem = screen.getByText(member.name); + const entityPanel = screen.getByTestId('explore_entity-information-panel'); + + await user.click(listItem); + const header = await waitFor(() => screen.getByText('Object Information')); + + expect(header).toBeInTheDocument(); + expect(entityPanel).toHaveTextContent(member.name); + }); + + it('renders a link to the explore page when member is clicked', async () => { + const { screen, user } = await setup(); + const member = assetGroupMembers.members[0]; + const listItem = screen.getByText(member.name); + + await user.click(listItem); + const link = screen.getByTestId('group-management_explore-link'); + expect(link).toBeInTheDocument(); + }); +}); diff --git a/cmd/ui/src/views/GroupManagement/GroupManagement.tsx b/cmd/ui/src/views/GroupManagement/GroupManagement.tsx new file mode 100644 index 0000000000..28a89302db --- /dev/null +++ b/cmd/ui/src/views/GroupManagement/GroupManagement.tsx @@ -0,0 +1,92 @@ +// 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 + +import EntityInfoPanel from '../Explore/EntityInfo/EntityInfoPanel'; +import { DropdownOption, GroupManagementContent, EntityKinds } from 'bh-shared-ui'; +import { SelectedNode } from 'src/ducks/entityinfo/types'; +import { useState } from 'react'; +import { AssetGroup, AssetGroupMember } from 'js-client-library'; +import { faGem } from '@fortawesome/free-solid-svg-icons'; +import { useSelector } from 'react-redux'; +import { Domain } from 'src/ducks/global/types'; +import { setSelectedNode } from 'src/ducks/entityinfo/actions'; +import { useNavigate } from 'react-router-dom'; +import { ROUTE_EXPLORE } from 'src/ducks/global/routes'; +import { sourceNodeSelected } from 'src/ducks/searchbar/actions'; +import { TIER_ZERO_LABEL, TIER_ZERO_TAG } from 'src/constants'; +import { useAppDispatch } from 'src/store'; +import { dataCollectionMessage } from '../QA/utils'; + +const GroupManagement = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + const globalDomain: Domain = useSelector((state: any) => state.global.options.domain); + + // Kept out of the shared UI due to diff between GraphNodeTypes across apps + const [openNode, setOpenNode] = useState(null); + + const handleClickMember = (member: AssetGroupMember) => { + setOpenNode({ + id: member.object_id, + type: member.primary_kind as EntityKinds, + name: member.name, + }); + }; + + const handleShowNodeInExplore = () => { + if (openNode) { + const searchNode = { + objectid: openNode.id, + label: openNode.name, + ...openNode, + }; + dispatch(sourceNodeSelected(searchNode)); + dispatch(setSelectedNode(openNode)); + + navigate(ROUTE_EXPLORE); + } + }; + + // Handle tier zero case + const mapAssetGroups = (assetGroups: AssetGroup[]): DropdownOption[] => { + return assetGroups.map((assetGroup) => { + const isTierZero = assetGroup.tag === TIER_ZERO_TAG; + return { + key: assetGroup.id, + value: isTierZero ? TIER_ZERO_LABEL : assetGroup.name, + icon: isTierZero ? faGem : undefined, + }; + }); + }; + + return ( + } + domainSelectorErrorMessage={<>Domains unavailable. {dataCollectionMessage}} + onShowNodeInExplore={handleShowNodeInExplore} + onClickMember={handleClickMember} + mapAssetGroups={mapAssetGroups} + /> + ); +}; + +export default GroupManagement; diff --git a/cmd/ui/src/views/QA/QA.tsx b/cmd/ui/src/views/QA/QA.tsx index 3dc8e7f44c..a3f39e79e0 100644 --- a/cmd/ui/src/views/QA/QA.tsx +++ b/cmd/ui/src/views/QA/QA.tsx @@ -26,6 +26,7 @@ import { import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'src/store'; +import { dataCollectionMessage } from './utils'; const QualityAssurance: React.FC = () => { const domain = useSelector((state: AppState) => state.global.options.domain); @@ -56,6 +57,8 @@ const QualityAssurance: React.FC = () => { } }; + const domainErrorMessage = <>Domains unavailable. {dataCollectionMessage}; + if (!contextType || (!contextId && (contextType === 'active-directory' || contextType === 'azure'))) { return ( { type: contextType, id: contextId, }} + errorMessage={domainErrorMessage} onChange={({ type, id }) => { setContextType(type); setContextId(id); @@ -76,15 +80,7 @@ const QualityAssurance: React.FC = () => { No Domain or Tenant Selected Select a domain or tenant to view data. If you are unable to select a domain, you may need to run - data collection first. See the{' '} - - Data Collection - {' '} - page to view instructions on how to begin data collection. + data collection first. {dataCollectionMessage} ); @@ -100,6 +96,7 @@ const QualityAssurance: React.FC = () => { type: contextType, id: contextId, }} + errorMessage={domainErrorMessage} onChange={({ type, id }) => { setContextType(type); setContextId(id); diff --git a/cmd/ui/src/views/QA/utils.tsx b/cmd/ui/src/views/QA/utils.tsx new file mode 100644 index 0000000000..f647269af8 --- /dev/null +++ b/cmd/ui/src/views/QA/utils.tsx @@ -0,0 +1,29 @@ +// 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 + +import { Link } from '@mui/material'; + +export const dataCollectionMessage = ( + <> + See the{' '} + + Data Collection + {' '} + page to view instructions on how to begin data collection. + +); diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupAutocomplete.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupAutocomplete.tsx new file mode 100644 index 0000000000..034a5eb36b --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupAutocomplete.tsx @@ -0,0 +1,128 @@ +// 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 + +import { Autocomplete, AutocompleteRenderInputParams, TextField } from '@mui/material'; +import { FC, HTMLAttributes, ReactNode, SyntheticEvent, useState } from 'react'; +import AutocompleteOption from './AutocompleteOption'; +import { AssetGroupChangelog, AssetGroupChangelogEntry, ChangelogAction } from './types'; +import { AssetGroup } from 'js-client-library'; +import { getEmptyResultsText, getKeywordAndTypeValues, useDebouncedValue, useSearch } from '../../hooks'; + +export const AUTOCOMPLETE_PLACEHOLDER = 'Add or Remove Members'; + +const AssetGroupAutocomplete: FC<{ + assetGroup: AssetGroup; + changelog: AssetGroupChangelog; + onChange: (event: any, value: AssetGroupChangelogEntry) => void; +}> = ({ assetGroup, changelog, onChange }) => { + const [searchInput, setSearchInput] = useState(''); + const debouncedInputValue = useDebouncedValue(searchInput, 250); + const { keyword, type } = getKeywordAndTypeValues(debouncedInputValue); + const { data, isLoading, isFetching, isError, error } = useSearch(keyword, type); + + const noOptionsText = getEmptyResultsText( + isLoading, + isFetching, + isError, + error, + debouncedInputValue, + type, + keyword, + data + ); + + const searchResultsWithActions = data?.map((result) => { + const resultInChangelog = changelog.find((member) => member.objectid === result.objectid); + const matchedSelector = assetGroup.Selectors.find((selector) => selector.selector === result.objectid); + + let action = ChangelogAction.ADD; + + if (result.system_tags?.includes(assetGroup.tag)) { + action = ChangelogAction.DEFAULT; + } + if (matchedSelector) { + action = ChangelogAction.REMOVE; + } + if (resultInChangelog) { + action = ChangelogAction.UNDO; + } + + return { ...result, action }; + }); + + const handleRenderInput = (params: AutocompleteRenderInputParams): ReactNode => { + return ( + + ); + }; + + const handleRenderOption = (props: HTMLAttributes, option: AssetGroupChangelogEntry): ReactNode => { + const actionLabels = { + [ChangelogAction.ADD]: 'Add', + [ChangelogAction.REMOVE]: 'Remove', + [ChangelogAction.DEFAULT]: 'Default Group Member', + [ChangelogAction.UNDO]: 'Undo', + }; + return ( + + ); + }; + + const handleInputChange = (_event: SyntheticEvent, value: string, reason: string): void => { + if (reason === 'reset') return; + setSearchInput(value); + }; + + const filterOptions = (options: AssetGroupChangelogEntry[]) => options; + const getOptionLabel = (option: AssetGroupChangelogEntry) => option.name || option.objectid; + const getOptionDisabled = (option: AssetGroupChangelogEntry) => option.action === ChangelogAction.DEFAULT; + + return ( + + renderInput={handleRenderInput} + renderOption={handleRenderOption} + onInputChange={handleInputChange} + onChange={onChange} + inputValue={searchInput} + filterOptions={filterOptions} + value={null} + options={searchResultsWithActions || []} + loading={isLoading || isFetching} + getOptionLabel={getOptionLabel} + getOptionDisabled={getOptionDisabled} + isOptionEqualToValue={() => false} + clearOnBlur + clearOnEscape + disableCloseOnSelect + noOptionsText={noOptionsText} + forcePopupIcon={false} + /> + ); +}; + +export default AssetGroupAutocomplete; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupChangelogTable.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupChangelogTable.tsx new file mode 100644 index 0000000000..b3c1923cb1 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupChangelogTable.tsx @@ -0,0 +1,108 @@ +// 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 + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + Box, + Button, + Grid, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from '@mui/material'; +import NodeIcon from '../NodeIcon'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { AssetGroupChangelogEntry } from './types'; +import { FC } from 'react'; + +const AssetGroupChangelogTable: FC<{ + addRows: AssetGroupChangelogEntry[]; + removeRows: AssetGroupChangelogEntry[]; + onRemove: (entry: AssetGroupChangelogEntry) => void; + onCancel: () => void; + onSubmit: () => void; +}> = ({ addRows, removeRows, onRemove, onCancel, onSubmit }) => { + return ( + <> + + + {addRows.length > 0 && ( + + )} + {removeRows.length > 0 && ( + + )} +
+
+ + + + + + + + + + + + ); +}; + +const AssetGroupChangelogRows: FC<{ + title: string; + rows: AssetGroupChangelogEntry[]; + onRemove: (entry: AssetGroupChangelogEntry) => void; +}> = ({ title, rows, onRemove }) => { + return ( + <> + + + {title} + + + + {rows.map((row) => ( + + + onRemove(row)}> + + + + + + {row.name} +
+ {row.objectid} +
+
+ ))} +
+ + ); +}; + +export default AssetGroupChangelogTable; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupEdit.test.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupEdit.test.tsx new file mode 100644 index 0000000000..a058f20857 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupEdit.test.tsx @@ -0,0 +1,121 @@ +// 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 + +import { setupServer } from 'msw/node'; +import { createMockAssetGroup, createMockAssetGroupMembers, createMockSearchResults } from '../../mocks/factories'; +import { act, render, waitFor } from '../../test-utils'; +import { AUTOCOMPLETE_PLACEHOLDER } from './AssetGroupAutocomplete'; +import AssetGroupEdit from './AssetGroupEdit'; +import { rest } from 'msw'; +import userEvent from '@testing-library/user-event'; + +const assetGroup = createMockAssetGroup(); +const assetGroupMembers = createMockAssetGroupMembers(); +const searchResults = createMockSearchResults(); + +const server = setupServer( + rest.get('/api/v2/asset-groups/1/members', (req, res, ctx) => { + return res( + ctx.json({ + count: assetGroupMembers.members.length, + limit: 100, + skip: 0, + data: assetGroupMembers, + }) + ); + }), + rest.get('/api/v2/search', (req, res, ctx) => { + return res( + ctx.json({ + data: searchResults, + }) + ); + }) +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('AssetGroupEdit', () => { + const setup = async () => { + const user = userEvent.setup(); + const screen = await act(async () => { + return render(); + }); + return { user, screen }; + }; + + it('should display a searchbox with a placeholder when rendered', async () => { + const { screen } = await setup(); + const input = screen.getByPlaceholderText(AUTOCOMPLETE_PLACEHOLDER); + expect(input).toBeInTheDocument(); + }); + + it('should display a total count of asset group members', async () => { + const { screen } = await setup(); + const count = screen.getByText('Total Count').nextSibling.textContent; + expect(count).toBe(assetGroupMembers.members.length.toString()); + }); + + it('should display search results when the user enters text', async () => { + const { screen, user } = await setup(); + const input = screen.getByRole('combobox'); + + await user.type(input, 'test'); + expect(input.value).toEqual('test'); + + const result = await waitFor(() => screen.getByText('00001.TESTLAB.LOCAL')); + expect(result).toBeInTheDocument(); + }); + + it('should add an option and display the changelog when it is clicked', async () => { + const { screen, user } = await setup(); + const selection = searchResults[0]; + + const input = screen.getByRole('combobox'); + await user.type(input, 'test'); + expect(input.value).toEqual('test'); + + const result = await waitFor(() => screen.getByText(selection.name)); + await user.click(result); + await user.keyboard('{Escape}'); + + expect(screen.getByText(selection.name)).toBeInTheDocument(); + expect(screen.getByText(selection.objectid)).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + expect(screen.getByText('Confirm Changes')).toBeInTheDocument(); + }); + + it('should remove the option from the changelog when the corresponding button is clicked', async () => { + const { screen, user } = await setup(); + const selection = searchResults[0]; + + const input = screen.getByRole('combobox'); + await user.type(input, 'test'); + expect(input.value).toEqual('test'); + + const result = await waitFor(() => screen.getByText(selection.name)); + await user.click(result); + await user.keyboard('{Escape}'); + + const removeButton = screen.getByText('xmark'); + await user.click(removeButton); + + expect(screen.queryByText(selection.name)).toBeNull(); + expect(screen.queryByText(selection.objectid)).toBeNull(); + }); +}); diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupEdit.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupEdit.tsx new file mode 100644 index 0000000000..9c814539e9 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AssetGroupEdit.tsx @@ -0,0 +1,160 @@ +// 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 + +import { Box, Paper } from '@mui/material'; +import { AssetGroup, AssetGroupMemberParams, UpdateAssetGroupSelectorRequest } from 'js-client-library'; +import { FC, useEffect, useState } from 'react'; +import { AssetGroupChangelog, AssetGroupChangelogEntry, ChangelogAction } from './types'; +import AssetGroupAutocomplete from './AssetGroupAutocomplete'; +import { SubHeader } from '../../views/Explore'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; +import { apiClient } from '../../utils'; +import AssetGroupChangelogTable from './AssetGroupChangelogTable'; +import { + ActiveDirectoryNodeKind, + ActiveDirectoryNodeKindToDisplay, + AzureNodeKind, + AzureNodeKindToDisplay, +} from '../../graphSchema'; +import { useNotifications } from '../../providers'; + +const AssetGroupEdit: FC<{ + assetGroup: AssetGroup; + filter: AssetGroupMemberParams; +}> = ({ assetGroup, filter }) => { + const [changelog, setChangelog] = useState([]); + const addRows = changelog.filter((entry) => entry.action === ChangelogAction.ADD); + const removeRows = changelog.filter((entry) => entry.action === ChangelogAction.REMOVE); + const { addNotification } = useNotifications(); + const queryClient = useQueryClient(); + + const handleUpdateAssetGroupChangelog = (_event: any, changelogEntry: AssetGroupChangelogEntry) => { + if (changelogEntry.action === ChangelogAction.ADD || changelogEntry.action === ChangelogAction.REMOVE) { + setChangelog([...changelog, changelogEntry]); + } + if (changelogEntry.action === ChangelogAction.UNDO) { + handleRemoveEntryFromChangelog(changelogEntry); + } + }; + + const mapChangelogToSelectors = (): UpdateAssetGroupSelectorRequest[] => { + return changelog.map((item) => { + return { + selector_name: item.objectid, + sid: item.objectid, + action: item.action === ChangelogAction.ADD ? 'add' : 'remove', + }; + }); + }; + + // Clear out changelog when group/domain changes + useEffect(() => setChangelog([]), [filter]); + + const mutation = useMutation({ + mutationFn: () => { + const selectors = mapChangelogToSelectors(); + return apiClient.updateAssetGroupSelector(assetGroup.id.toString(), selectors); + }, + onSuccess: () => { + setChangelog([]); + + // refetch all page data after updating group membership + queryClient.invalidateQueries({ queryKey: ['listAssetGroups'] }); + queryClient.invalidateQueries({ queryKey: ['listAssetGroupMembers'] }); + queryClient.invalidateQueries({ queryKey: ['countAssetGroupMembers'] }); + queryClient.resetQueries({ queryKey: ['search'] }); + + addNotification('Update successful.', 'AssetGroupUpdateSuccess'); + }, + onError: (error) => { + console.error(error); + setChangelog([]); + addNotification('Unknown error, group was not updated', 'AssetGroupUpdateError'); + }, + }); + + const handleRemoveEntryFromChangelog = (entry: AssetGroupChangelogEntry) => { + setChangelog((prev) => prev.filter((item) => item.objectid !== entry.objectid)); + }; + + return ( + + + + {changelog.length > 0 && ( + setChangelog([])} + onSubmit={() => mutation.mutate()} + /> + )} + {Object.values(ActiveDirectoryNodeKind).map((kind) => { + const filterByKind = { ...filter, primary_kind: `eq:${kind}` }; + const label = ActiveDirectoryNodeKindToDisplay(kind) || ''; + return ( + + ); + })} + {Object.values(AzureNodeKind).map((kind) => { + const filterByKind = { ...filter, primary_kind: `eq:${kind}` }; + const label = AzureNodeKindToDisplay(kind) || ''; + return ( + + ); + })} + + ); +}; + +const FilteredMemberCountDisplay: FC<{ + assetGroupId: number; + label: string; + filter: AssetGroupMemberParams; +}> = ({ assetGroupId, label, filter }) => { + const { + data: count, + isError, + isLoading, + } = useQuery(['countAssetGroupMembers', assetGroupId, filter], ({ signal }) => + apiClient.listAssetGroupMembers(assetGroupId.toString(), filter, { signal }).then((res) => res.data.count) + ); + + const hasValidCount = !isLoading && !isError && count && count > 0; + + if (hasValidCount) { + return ; + } else { + return null; + } +}; + +export default AssetGroupEdit; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AutocompleteOption.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AutocompleteOption.tsx new file mode 100644 index 0000000000..86435183b2 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/AutocompleteOption.tsx @@ -0,0 +1,58 @@ +// 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 + +import { Box, Fade, ListItem, Tooltip, Typography } from '@mui/material'; +import { FC, HTMLAttributes } from 'react'; +import NodeIcon from '../NodeIcon'; + +const AutocompleteOption: FC<{ + props: HTMLAttributes; + id: string; + name?: string; + type: string; + actionLabel?: string; +}> = ({ props, id, name, type, actionLabel }) => { + return ( + + {actionLabel} + + + + + {name || id} + + + + + ); +}; + +export default AutocompleteOption; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/index.ts b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/index.ts new file mode 100644 index 0000000000..3b65499642 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/index.ts @@ -0,0 +1,21 @@ +// 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 + +export { default } from './AssetGroupEdit'; +export { default as AssetGroupChangelogTable } from './AssetGroupChangelogTable'; +export { default as AssetGroupAutocomplete } from './AssetGroupAutocomplete'; +export { default as AutocompleteOption } from './AutocompleteOption'; +export * from './types'; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/types.ts b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/types.ts new file mode 100644 index 0000000000..bf92ea2fae --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupEdit/types.ts @@ -0,0 +1,32 @@ +// 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 + +export enum ChangelogAction { + ADD, + REMOVE, + DEFAULT, + UNDO, +} + +export type MemberData = { + objectid: string; + name: string; + type: string; +}; + +export type AssetGroupChangelogEntry = MemberData & { action: ChangelogAction }; + +export type AssetGroupChangelog = AssetGroupChangelogEntry[]; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/AssetGroupMemberList.test.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/AssetGroupMemberList.test.tsx new file mode 100644 index 0000000000..5fb818f3cb --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/AssetGroupMemberList.test.tsx @@ -0,0 +1,76 @@ +// 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 + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import AssetGroupMemberList from './AssetGroupMemberList'; +import { render, waitFor } from '../../test-utils'; +import { createMockAssetGroup, createMockAssetGroupMembers } from '../../mocks/factories'; +import userEvent from '@testing-library/user-event'; + +const assetGroup = createMockAssetGroup(); +const assetGroupMembers = createMockAssetGroupMembers(); + +const server = setupServer( + rest.get('/api/v2/asset-groups/1/members', (req, res, ctx) => { + return res( + ctx.json({ + count: assetGroupMembers.members.length, + limit: 100, + skip: 0, + data: assetGroupMembers, + }) + ); + }) +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('AssetGroupMemberList', () => { + const setup = () => { + const handleSelectMember = vi.fn(); + const user = userEvent.setup(); + const screen = render( + + ); + return { screen, user, handleSelectMember }; + }; + + it('Should display headers for member name and count', () => { + const { screen } = setup(); + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText('Custom Member')).toBeInTheDocument(); + }); + + it('Should display a list of the asset group members', () => { + const { screen } = setup(); + waitFor(() => { + for (const member of assetGroupMembers.members) { + expect(screen.getByText(member.name)).toBeInTheDocument(); + } + }); + }); + + it('Should call handler when a member is clicked', async () => { + const { screen, user, handleSelectMember } = setup(); + const testMember = assetGroupMembers.members[0]; + const entry = await waitFor(() => screen.getByText(testMember.name)); + await user.click(entry); + expect(handleSelectMember).toHaveBeenCalledWith(testMember); + }); +}); diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/AssetGroupMemberList.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/AssetGroupMemberList.tsx new file mode 100644 index 0000000000..0cc924da72 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/AssetGroupMemberList.tsx @@ -0,0 +1,202 @@ +// 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 + +import { + Box, + Paper, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableFooter, + TableHead, + TablePagination, + TableRow, + Typography, + useTheme, +} from '@mui/material'; +import { FC, useEffect, useState } from 'react'; +import NodeIcon from '../NodeIcon'; +import { AssetGroup, AssetGroupMember, AssetGroupMemberParams } from 'js-client-library'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { useQuery } from 'react-query'; +import { apiClient } from '../../utils'; + +const AssetGroupMemberList: FC<{ + assetGroup: AssetGroup | null; + filter: AssetGroupMemberParams; + onSelectMember: (member: any) => void; +}> = ({ assetGroup, filter, onSelectMember }) => { + const theme = useTheme(); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(25); + const [count, setCount] = useState(0); + + const { data, isLoading, isPreviousData, isSuccess } = useQuery( + ['listAssetGroupMembers', assetGroup, filter, page, rowsPerPage], + ({ signal }) => { + const paginatedFilter = { + skip: page * rowsPerPage, + limit: rowsPerPage, + // we could make this user selected in the future + sort_by: 'name', + ...filter, + }; + return apiClient.listAssetGroupMembers(`${assetGroup?.id}`, paginatedFilter, { signal }).then((res) => { + setCount(res.data.count); + return res.data.data.members; + }); + }, + { + enabled: !!assetGroup, + keepPreviousData: true, + } + ); + + // Prevents an error that occurs if you try to query with a "skip" value greater than the member count of the current group + useEffect(() => setPage(0), [assetGroup, filter]); + + const getLoadingRows = (count: number) => { + const rows = []; + for (let i = 0; i < count; i++) { + rows.push( + + + + + + + + + ); + } + return rows; + }; + + return ( + + + + + + + + + Name + + Custom Member + + + + + {isLoading && getLoadingRows(10)} + {isSuccess && + !!data.length && + data.map((member) => ( + + ))} + {isSuccess && data.length === 0 && ( + + + No members in selected Asset Group + + + )} + + {isSuccess && !!data.length && ( + + + setPage(page)} + onRowsPerPageChange={(event) => setRowsPerPage(parseInt(event.target.value))} + /> + + + )} +
+
+ ); +}; + +const AssetGroupMemberRow: FC<{ + member: AssetGroupMember; + disabled: boolean; + onClick: (member: AssetGroupMember) => void; +}> = ({ member, disabled, onClick }) => { + const theme = useTheme(); + + const disabledRowStyles = { opacity: '0.5' }; + + const rowStyles = { + '&:hover': { + backgroundColor: theme.palette.action.hover, + cursor: 'pointer', + }, + }; + + const handleClick = () => { + if (!disabled) onClick(member); + }; + + return ( + + + + + + {member.name} + + + + + {member.custom_member ? ( + + ) : ( + + )} + + + ); +}; + +export default AssetGroupMemberList; diff --git a/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/index.tsx b/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/index.tsx new file mode 100644 index 0000000000..917f02964f --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/AssetGroupMemberList/index.tsx @@ -0,0 +1,17 @@ +// 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 + +export { default } from './AssetGroupMemberList'; diff --git a/packages/javascript/bh-shared-ui/src/components/DropdownSelector/DropdownSelector.tsx b/packages/javascript/bh-shared-ui/src/components/DropdownSelector/DropdownSelector.tsx new file mode 100644 index 0000000000..571b3909fa --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/DropdownSelector/DropdownSelector.tsx @@ -0,0 +1,110 @@ +// 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 + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Box, Button, MenuItem, Popover, Tooltip, Typography } from '@mui/material'; +import { FC, useState } from 'react'; +import { DropdownOption } from './types'; + +const DropdownSelector: FC<{ + options: DropdownOption[]; + selectedText: string; + fullWidth?: boolean; + onChange: (selection: DropdownOption) => void; +}> = ({ options, selectedText, fullWidth, onChange }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (e: any) => { + setAnchorEl(e.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + + + + {options.map((option) => { + return ( + { + onChange(option); + handleClose(); + }}> + + + {option.value} + + + {option.icon && ( + + )} + + ); + })} + + + ); +}; + +export default DropdownSelector; diff --git a/packages/javascript/bh-shared-ui/src/components/DropdownSelector/index.ts b/packages/javascript/bh-shared-ui/src/components/DropdownSelector/index.ts new file mode 100644 index 0000000000..f3bf37420d --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/DropdownSelector/index.ts @@ -0,0 +1,19 @@ +// 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 + +export * from './DropdownSelector'; +export { default } from './DropdownSelector'; +export * from './types'; diff --git a/packages/javascript/bh-shared-ui/src/components/DropdownSelector/types.ts b/packages/javascript/bh-shared-ui/src/components/DropdownSelector/types.ts new file mode 100644 index 0000000000..cd4e6f4070 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/DropdownSelector/types.ts @@ -0,0 +1,23 @@ +// 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 + +import { IconDefinition } from '@fortawesome/free-solid-svg-icons'; + +export type DropdownOption = { + key: number; + value: string; + icon?: IconDefinition; +}; diff --git a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx new file mode 100644 index 0000000000..689ad3cfe8 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx @@ -0,0 +1,159 @@ +// 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 + +import { AssetGroup, AssetGroupMember, AssetGroupMemberParams } from 'js-client-library'; +import { FC, ReactNode, useEffect, useState } from 'react'; +import DropdownSelector, { DropdownOption } from '../DropdownSelector'; +import { Box, Button, Grid, Paper, Typography, useTheme } from '@mui/material'; +import { useQuery } from 'react-query'; +import { apiClient } from '../../utils'; +import { faExternalLink } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import AssetGroupEdit from '../AssetGroupEdit'; +import AssetGroupMemberList from '../AssetGroupMemberList'; +import { SelectedDomain } from './types'; +import DataSelector from '../../views/DataQuality/DataSelector'; + +// Top level layout and shared logic for the Group Management page +const GroupManagementContent: FC<{ + globalDomain: SelectedDomain; + showExplorePageLink: boolean; + tierZeroLabel: string; + tierZeroTag: string; + entityPanelComponent: ReactNode; + domainSelectorErrorMessage: ReactNode; + onShowNodeInExplore: () => void; + onClickMember: (member: AssetGroupMember) => void; + mapAssetGroups: (assetGroups: AssetGroup[]) => DropdownOption[]; +}> = ({ + globalDomain, + showExplorePageLink, + tierZeroLabel, + tierZeroTag, + entityPanelComponent, + domainSelectorErrorMessage, + onShowNodeInExplore, + onClickMember, + mapAssetGroups, +}) => { + const theme = useTheme(); + + const [selectedDomain, setSelectedDomain] = useState(null); + const [selectedAssetGroupId, setSelectedAssetGroupId] = useState(null); + const [filterParams, setFilterParams] = useState({}); + + const setInitialGroup = (data: AssetGroup[]) => { + if (!selectedAssetGroupId && data?.length) { + const initialGroup = data.find((group) => group.tag === tierZeroTag) || data[0]; + setSelectedAssetGroupId(initialGroup.id); + } + }; + + const listAssetGroups = useQuery( + ['listAssetGroups'], + () => apiClient.listAssetGroups().then((res) => res.data.data.asset_groups), + { onSuccess: setInitialGroup } + ); + + const selectedAssetGroup = listAssetGroups.data?.find((group) => group.id === selectedAssetGroupId) || null; + + const handleAssetGroupSelectorChange = (selectedAssetGroup: DropdownOption) => { + const selected = listAssetGroups.data?.find((assetGroup) => assetGroup.id === selectedAssetGroup.key); + if (selected) setSelectedAssetGroupId(selected.id); + }; + + const getAssetGroupSelectorLabel = (): string => { + if (selectedAssetGroup?.tag === tierZeroTag) return tierZeroLabel; + return selectedAssetGroup?.name || 'Select a Group'; + }; + + // Start building a filter query for members that gets passed down to AssetGroupMemberList to make the request + useEffect(() => { + const filterDomain = selectedDomain || globalDomain; + const filter: AssetGroupMemberParams = {}; + if (filterDomain?.type === 'active-directory-platform') { + filter.environment_kind = 'eq:Domain'; + } else if (filterDomain?.type === 'azure-platform') { + filter.environment_kind = 'eq:AZTenant'; + } else { + filter.environment_id = `eq:${filterDomain?.id}`; + } + setFilterParams(filter); + }, [selectedDomain, globalDomain, selectedAssetGroupId]); + + const selectorLabelStyles = { display: { xs: 'none', xl: 'flex' } }; + + return ( + + + + + + + Group: + + + + + + Environment: + + + setSelectedDomain({ ...selection })} + fullWidth={true} + /> + + + + {selectedAssetGroup && } + + + + + + {/* CSS calc accounts for the height of the link button */} + {entityPanelComponent} + {showExplorePageLink && ( + + )} + + + + ); +}; + +export default GroupManagementContent; diff --git a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/index.ts b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/index.ts new file mode 100644 index 0000000000..c47b7468bb --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/index.ts @@ -0,0 +1,20 @@ +// 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 + +import GroupManagementContent from './GroupManagementContent'; + +export default GroupManagementContent; +export * from './types'; diff --git a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts new file mode 100644 index 0000000000..f25ccc51c3 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts @@ -0,0 +1,20 @@ +// 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 + +export type SelectedDomain = { + id: string | null; + type: string | null; +}; diff --git a/packages/javascript/bh-shared-ui/src/components/index.ts b/packages/javascript/bh-shared-ui/src/components/index.ts index c08fe9584b..6444e4113d 100644 --- a/packages/javascript/bh-shared-ui/src/components/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/index.ts @@ -122,3 +122,15 @@ export { default as TextWithFallback } from './TextWithFallback'; export * from './UserTokenManagementDialog'; export { default as UserTokenManagementDialog } from './UserTokenManagementDialog'; + +export * from './AssetGroupMemberList'; +export { default as AssetGroupMemberList } from './AssetGroupMemberList'; + +export * from './DropdownSelector'; +export { default as DropdownSelector } from './DropdownSelector'; + +export * from './AssetGroupEdit'; +export { default as AssetGroupEdit } from './AssetGroupEdit'; + +export * from './GroupManagementContent'; +export { default as GroupManagementContent } from './GroupManagementContent'; diff --git a/packages/javascript/bh-shared-ui/src/hooks/index.ts b/packages/javascript/bh-shared-ui/src/hooks/index.ts index fef3bd37a1..c6a71e4eeb 100644 --- a/packages/javascript/bh-shared-ui/src/hooks/index.ts +++ b/packages/javascript/bh-shared-ui/src/hooks/index.ts @@ -18,6 +18,10 @@ export { default as useAvailableDomains } from './useAvailableDomains'; export { default as useOnClickOutside } from './useOnClickOutside'; +export { default as useDebouncedValue } from './useDebouncedValue'; + +export * from './useSearch'; + export * from './useDataQualityStats'; export * from './useSavedQueries'; diff --git a/cmd/ui/src/hooks/useDebouncedValue.tsx b/packages/javascript/bh-shared-ui/src/hooks/useDebouncedValue.tsx similarity index 91% rename from cmd/ui/src/hooks/useDebouncedValue.tsx rename to packages/javascript/bh-shared-ui/src/hooks/useDebouncedValue.tsx index 9db8a7efb2..27b048f410 100644 --- a/cmd/ui/src/hooks/useDebouncedValue.tsx +++ b/packages/javascript/bh-shared-ui/src/hooks/useDebouncedValue.tsx @@ -16,7 +16,7 @@ import { useState, useEffect } from 'react'; -export const useDebouncedValue = (value: any, delay: number) => { +const useDebouncedValue = (value: any, delay: number) => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { @@ -30,3 +30,5 @@ export const useDebouncedValue = (value: any, delay: number) => { return debouncedValue; }; + +export default useDebouncedValue; diff --git a/cmd/ui/src/hooks/useSearch/index.ts b/packages/javascript/bh-shared-ui/src/hooks/useSearch/index.ts similarity index 100% rename from cmd/ui/src/hooks/useSearch/index.ts rename to packages/javascript/bh-shared-ui/src/hooks/useSearch/index.ts diff --git a/cmd/ui/src/hooks/useSearch/useSearch.test.ts b/packages/javascript/bh-shared-ui/src/hooks/useSearch/useSearch.test.ts similarity index 97% rename from cmd/ui/src/hooks/useSearch/useSearch.test.ts rename to packages/javascript/bh-shared-ui/src/hooks/useSearch/useSearch.test.ts index 8497c30ae0..0777a8cfb0 100644 --- a/cmd/ui/src/hooks/useSearch/useSearch.test.ts +++ b/packages/javascript/bh-shared-ui/src/hooks/useSearch/useSearch.test.ts @@ -14,8 +14,8 @@ // // SPDX-License-Identifier: Apache-2.0 -import { getEmptyResultsText, getKeywordAndTypeValues } from 'src/hooks/useSearch'; -import { ActiveDirectoryNodeKind } from 'bh-shared-ui'; +import { ActiveDirectoryNodeKind } from '../../graphSchema'; +import { getEmptyResultsText, getKeywordAndTypeValues } from './useSearch'; describe('Getting the text for the disabled item display for a search when there are no results', () => { describe('Loading states', () => { diff --git a/cmd/ui/src/hooks/useSearch/useSearch.tsx b/packages/javascript/bh-shared-ui/src/hooks/useSearch/useSearch.tsx similarity index 82% rename from cmd/ui/src/hooks/useSearch/useSearch.tsx rename to packages/javascript/bh-shared-ui/src/hooks/useSearch/useSearch.tsx index b43a62f191..e5c1da8978 100644 --- a/cmd/ui/src/hooks/useSearch/useSearch.tsx +++ b/packages/javascript/bh-shared-ui/src/hooks/useSearch/useSearch.tsx @@ -15,14 +15,14 @@ // SPDX-License-Identifier: Apache-2.0 import { useQuery } from 'react-query'; -import { apiClient } from 'bh-shared-ui'; -import { ActiveDirectoryNodeKind, AzureNodeKind } from 'bh-shared-ui'; -import { validateNodeType } from 'src/utils'; +import { apiClient } from '../../utils'; +import { ActiveDirectoryNodeKind, AzureNodeKind } from '../../graphSchema'; export type SearchResult = { distinguishedname?: string; name: string; objectid: string; + system_tags?: string; type: string; }; @@ -67,6 +67,21 @@ export const getKeywordAndTypeValues = ( return { keyword: keyword, type: type }; }; +const validateNodeType = (type: string): ActiveDirectoryNodeKind | AzureNodeKind | undefined => { + let result = undefined; + Object.values(ActiveDirectoryNodeKind).forEach((activeDirectoryType: string) => { + if (activeDirectoryType.localeCompare(type, undefined, { sensitivity: 'base' }) === 0) + result = activeDirectoryType as ActiveDirectoryNodeKind; + }); + + Object.values(AzureNodeKind).forEach((azureType: string) => { + if (azureType.localeCompare(type, undefined, { sensitivity: 'base' }) === 0) + result = azureType as AzureNodeKind; + }); + + return result; +}; + const getErrorText = (error: any): string => { if (error.response?.status === 504) return 'Search has timed out. Please try again.'; else return 'An error has occurred. Please try again.'; diff --git a/packages/javascript/bh-shared-ui/src/index.ts b/packages/javascript/bh-shared-ui/src/index.ts index 12eaa6ee1b..6e064cd4df 100644 --- a/packages/javascript/bh-shared-ui/src/index.ts +++ b/packages/javascript/bh-shared-ui/src/index.ts @@ -36,3 +36,5 @@ export * from './graphSchema'; export * from './views'; export * from './store'; + +export * from './mocks'; diff --git a/packages/javascript/bh-shared-ui/src/mocks/factories.ts b/packages/javascript/bh-shared-ui/src/mocks/factories.ts new file mode 100644 index 0000000000..bb35a4aa60 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/mocks/factories.ts @@ -0,0 +1,84 @@ +// 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 + +import { AssetGroup, AssetGroupMember } from 'js-client-library'; +import { SearchResults } from '../hooks'; + +export const createMockAssetGroupMembers = (): { members: AssetGroupMember[] } => { + return { + members: [ + { + asset_group_id: 1, + object_id: '00000-00001', + primary_kind: 'User', + kinds: ['User', 'Base'], + environment_id: '00000-00000-00001', + environment_kind: 'Domain', + name: 'USER_00001@TESTLAB.LOCAL', + custom_member: false, + }, + { + asset_group_id: 1, + object_id: '00000-00002', + primary_kind: 'Computer', + kinds: ['Computer', 'Base'], + environment_id: '00000-00000-00001', + environment_kind: 'Domain', + name: 'COMPUTER_00001@TESTLAB.LOCAL', + custom_member: false, + }, + { + asset_group_id: 1, + object_id: '00000-00003', + primary_kind: 'GPO', + kinds: ['GPO', 'Base'], + environment_id: '00000-00000-00001', + environment_kind: 'Domain', + name: 'GPO_00001@TESTLAB.LOCAL', + custom_member: true, + }, + ], + }; +}; + +export const createMockAssetGroup = (): AssetGroup => { + return { + id: 1, + name: 'Admin Tier Zero', + tag: 'admin_tier_0', + member_count: 3, + system_group: true, + Selectors: [], + created_at: '2023-10-18T16:19:25.26533Z', + updated_at: '2023-10-18T16:19:25.26533Z', + deleted_at: { + Time: '0001-01-01T00:00:00Z', + Valid: false, + }, + }; +}; + +export const createMockSearchResults = (): SearchResults => { + return [ + { + objectid: '00000-00000-00000-00001', + type: 'Computer', + name: '00001.TESTLAB.LOCAL', + distinguishedname: '', + system_tags: '', + }, + ]; +}; diff --git a/packages/javascript/bh-shared-ui/src/mocks/index.ts b/packages/javascript/bh-shared-ui/src/mocks/index.ts new file mode 100644 index 0000000000..3ea6a2b454 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/mocks/index.ts @@ -0,0 +1,17 @@ +// 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 + +export * from './factories'; diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.test.tsx b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.test.tsx index 3876924bb0..0935f9f483 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.test.tsx @@ -258,12 +258,14 @@ const testDomains = [ }, ]; +const errorMessage = <>Domains unavailable; + describe('Context Selector', () => { it('should render with a full list of multiple tenants and domains', async () => { const user = userEvent.setup(); const testOnChange = vi.fn(); const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' }; - render(); + render(); const contextSelector = await screen.findByTestId('data-quality_context-selector'); expect(contextSelector).toBeInTheDocument(); @@ -288,7 +290,7 @@ describe('Context Selector', () => { const user = userEvent.setup(); const testOnChange = vi.fn(); const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' }; - render(); + render(); const contextSelector = await screen.findByTestId('data-quality_context-selector'); await user.click(contextSelector); @@ -337,7 +339,7 @@ describe('Context Selector', () => { const user = userEvent.setup(); const testOnChange = vi.fn(); const testValue = { type: 'azure', id: 'd1993a1b-55c1-4668-9393-ddfffb6ab639' }; - render(); + render(); const contextSelector = await screen.findByTestId('data-quality_context-selector'); @@ -366,9 +368,10 @@ describe('Context Selector Error', () => { it('should display an error message if data does not return from the API', async () => { const testOnChange = vi.fn(); + const testErrorMessage = 'test error message'; const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' }; - render(); + render({testErrorMessage}} />); - expect(await screen.findByText('Data Collection')).toBeInTheDocument(); + expect(await screen.findByText(testErrorMessage)).toBeInTheDocument(); }); }); diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.tsx b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.tsx index c074e4516e..d6cafaf107 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.tsx +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/DataSelector.tsx @@ -21,7 +21,6 @@ import { Box, Button, Divider, - Link, MenuItem, Popover, Skeleton, @@ -30,31 +29,21 @@ import { Typography, } from '@mui/material'; import { useAvailableDomains, Domain } from '../../../hooks'; -import React, { useState } from 'react'; +import React, { ReactNode, useState } from 'react'; const DataSelector: React.FC<{ value: { type: string | null; id: string | null }; + errorMessage: ReactNode; onChange?: (newValue: { type: string; id: string | null }) => void; fullWidth?: boolean; -}> = ({ value, onChange = () => {}, fullWidth = false }) => { +}> = ({ value, errorMessage, onChange = () => {}, fullWidth = false }) => { const [anchorEl, setAnchorEl] = useState(null); const [searchInput, setSearchInput] = useState(''); const { data, isLoading, isError } = useAvailableDomains(); if (isLoading) return ; - if (isError) - return ( - - Domains unavailable. See the{' '} - - Data Collection - {' '} - page to view instructions on how to begin data collection. - - ); + if (isError) return {errorMessage}; const handleClick = (event: any) => { setAnchorEl(event.currentTarget); diff --git a/cmd/ui/src/views/Explore/InfoStyles/CollapsibleSection.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/CollapsibleSection.tsx similarity index 100% rename from cmd/ui/src/views/Explore/InfoStyles/CollapsibleSection.tsx rename to packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/CollapsibleSection.tsx diff --git a/cmd/ui/src/views/Explore/InfoStyles/Header.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/Header.tsx similarity index 100% rename from cmd/ui/src/views/Explore/InfoStyles/Header.tsx rename to packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/Header.tsx diff --git a/cmd/ui/src/views/Explore/InfoStyles/Pane.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/Pane.tsx similarity index 95% rename from cmd/ui/src/views/Explore/InfoStyles/Pane.tsx rename to packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/Pane.tsx index a8b075f444..06e251172c 100644 --- a/cmd/ui/src/views/Explore/InfoStyles/Pane.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/Pane.tsx @@ -22,8 +22,7 @@ const usePaneStyles = makeStyles((theme: Theme) => ({ display: 'flex', flexDirection: 'column', pointerEvents: 'none', - margin: theme.spacing(0, 4, 2, 2), - maxHeight: '95%', + overflowY: 'hidden', }, headerPaperRoot: { backgroundColor: theme.palette.background.paper, diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/index.ts b/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/index.ts new file mode 100644 index 0000000000..6b3e140399 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/views/Explore/InfoStyles/index.ts @@ -0,0 +1,19 @@ +// 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 + +export { default as useCollapsibleSectionStyles } from './CollapsibleSection'; +export { default as useHeaderStyles } from './Header'; +export { default as usePaneStyles } from './Pane'; diff --git a/cmd/ui/src/views/Explore/fragments.test.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx similarity index 97% rename from cmd/ui/src/views/Explore/fragments.test.tsx rename to packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx index 0d240eb717..e4d347596c 100644 --- a/cmd/ui/src/views/Explore/fragments.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx @@ -15,7 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Field } from './fragments'; -import { render, screen } from 'src/test-utils'; +import { render, screen } from '../../test-utils'; describe('Field', () => { it('should render a Field when the provided value is false', () => { diff --git a/cmd/ui/src/views/Explore/fragments.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx similarity index 66% rename from cmd/ui/src/views/Explore/fragments.tsx rename to packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx index 363f3e9cba..44010424c7 100644 --- a/cmd/ui/src/views/Explore/fragments.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx @@ -15,14 +15,9 @@ // SPDX-License-Identifier: Apache-2.0 import { Alert, Box, CircularProgress, Typography } from '@mui/material'; -import { AzureNodeKind, EntityField, EntityKinds, NodeIcon, format } from 'bh-shared-ui'; -import isEmpty from 'lodash/isEmpty'; +import useCollapsibleSectionStyles from './InfoStyles/CollapsibleSection'; import React, { PropsWithChildren } from 'react'; -import { TIER_ZERO_TAG } from 'src/constants'; -import { sourceNodeSelected } from 'src/ducks/searchbar/actions'; - -import { useAppDispatch } from 'src/store'; -import useCollapsibleSectionStyles from 'src/views/Explore/InfoStyles/CollapsibleSection'; +import { EntityField, format } from '../../utils'; const exclusionList = [ 'gid', @@ -108,7 +103,7 @@ export const Field: React.FC = (entityField) => { value === undefined || value === '' || (Array.isArray(value) && value.length === 0) || - (typeof value === 'object' && isEmpty(value)) + (typeof value === 'object' && Object.keys(value).length === 0) ) return null; @@ -152,70 +147,6 @@ export const Field: React.FC = (entityField) => { return <>{content}; }; -interface BasicObjectInfoFieldsProps { - objectid: string; - displayname?: string; - system_tags?: string; - service_principal_id?: string; - noderesourcegroupid?: string; - name?: string; -} - -const RelatedKindField = (fieldLabel: string, relatedKind: EntityKinds, id: string, name?: string) => { - const dispatch = useAppDispatch(); - return ( - - - {fieldLabel} - -
- - - { - dispatch( - sourceNodeSelected({ - objectid: id, - type: relatedKind, - name: name || '', - }) - ); - }} - style={{ cursor: 'pointer' }} - overflow='hidden' - textOverflow='ellipsis' - title={id}> - {id} - - -
- ); -}; - -export const BasicObjectInfoFields: React.FC = (props): JSX.Element => { - return ( - <> - {props.system_tags?.includes(TIER_ZERO_TAG) && } - {props.displayname && } - - {props.service_principal_id && - RelatedKindField( - 'Service Principal ID:', - AzureNodeKind.ServicePrincipal, - props.service_principal_id, - props.name - )} - {props.noderesourcegroupid && - RelatedKindField( - 'Node Resource Group ID:', - AzureNodeKind.ResourceGroup, - props.noderesourcegroupid, - props.name - )} - - ); -}; - export const ObjectInfoFields: React.FC<{ fields: EntityField[] }> = ({ fields }): JSX.Element => { const filteredFields = filterNegatedFields(fields); diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/index.ts b/packages/javascript/bh-shared-ui/src/views/Explore/index.ts new file mode 100644 index 0000000000..03c10fd014 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/views/Explore/index.ts @@ -0,0 +1,18 @@ +// 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 + +export * from './fragments'; +export * from './InfoStyles'; diff --git a/packages/javascript/bh-shared-ui/src/views/index.ts b/packages/javascript/bh-shared-ui/src/views/index.ts index 28bb7aa19b..833ed54dc5 100644 --- a/packages/javascript/bh-shared-ui/src/views/index.ts +++ b/packages/javascript/bh-shared-ui/src/views/index.ts @@ -16,6 +16,8 @@ export { default as UserProfile } from './UserProfile'; +export * from './Explore'; + export * from './DataQuality'; export * from './Explore/ExploreSearch'; diff --git a/packages/javascript/js-client-library/src/types.ts b/packages/javascript/js-client-library/src/types.ts index c16387ab18..d9f70bbe65 100644 --- a/packages/javascript/js-client-library/src/types.ts +++ b/packages/javascript/js-client-library/src/types.ts @@ -82,7 +82,6 @@ export interface CreateScheduledSharpHoundJobRequest { all_trusted_domains: boolean; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface export type CreateScheduledAzureHoundJobRequest = Record; export type CreateScheduledJobRequest = CreateScheduledSharpHoundJobRequest | CreateScheduledAzureHoundJobRequest;