From 3c6050338df8ecb5d60f666964bbdec67f27a84c Mon Sep 17 00:00:00 2001 From: Ben Waples Date: Thu, 23 Jan 2025 10:40:55 -0800 Subject: [PATCH] BED-5133 (#1048) * refactor: define Domain type in js-client * chore: add functionality to useAvailableDomains and leave a couple notes * refactor: DataSelectorValueType and improve domain types * fix: type export * fix: index export * feat: update posture api clients to match contract * refactor: remove URLSearchParms for axios tool instead * chore: remove comments * fix: getPostureHistory api endpoint * fix: type update --- cmd/ui/src/ducks/global/actions.ts | 3 +- cmd/ui/src/ducks/global/types.ts | 9 +--- cmd/ui/src/views/Content.tsx | 1 - cmd/ui/src/views/Explore/GraphView.tsx | 4 +- cmd/ui/src/views/QA/QA.tsx | 3 +- .../GroupManagementContent.tsx | 2 +- .../GroupManagementContent/types.ts | 4 +- .../src/hooks/useAvailableDomains.tsx | 19 +++++-- .../src/hooks/useDataQualityStats.tsx | 8 --- .../bh-shared-ui/src/utils/types.ts | 5 ++ .../DataSelector/DataSelector.test.tsx | 10 ++-- .../DataQuality/DataSelector/DataSelector.tsx | 10 ++-- .../DataSelector/{index.tsx => index.ts} | 5 +- .../views/DataQuality/DataSelector/types.ts | 20 ++++++++ .../src/views/DataQuality/index.ts | 4 +- .../js-client-library/src/client.ts | 49 ++++++------------- .../js-client-library/src/responses.ts | 8 +++ 17 files changed, 90 insertions(+), 74 deletions(-) rename packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/{index.tsx => index.ts} (88%) create mode 100644 packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/types.ts diff --git a/cmd/ui/src/ducks/global/actions.ts b/cmd/ui/src/ducks/global/actions.ts index 5a88df423c..beba662264 100644 --- a/cmd/ui/src/ducks/global/actions.ts +++ b/cmd/ui/src/ducks/global/actions.ts @@ -15,6 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { OptionsObject, SnackbarKey } from 'notistack'; import * as types from './types'; +import { Domain } from 'js-client-library'; export const removeSnackbar = (key: SnackbarKey): types.GlobalViewActionTypes => { return { @@ -63,7 +64,7 @@ export const setExpanded = (expanded: { [key: string]: symbol[] }): types.Global }; }; -export const setDomain = (domain: types.Domain | null): types.GlobalOptionsActionTypes => { +export const setDomain = (domain: Domain | null): types.GlobalOptionsActionTypes => { return { type: types.GLOBAL_SET_DOMAIN, domain, diff --git a/cmd/ui/src/ducks/global/types.ts b/cmd/ui/src/ducks/global/types.ts index 62440a8f08..70184aad26 100644 --- a/cmd/ui/src/ducks/global/types.ts +++ b/cmd/ui/src/ducks/global/types.ts @@ -14,6 +14,7 @@ // // SPDX-License-Identifier: Apache-2.0 import { Notification } from 'bh-shared-ui'; +import { Domain } from 'js-client-library'; import { SnackbarKey } from 'notistack'; const GLOBAL_ADD_SNACKBAR = 'app/global/ADDSNACKBAR'; @@ -107,14 +108,6 @@ export type GlobalOptionsActionTypes = | SetAssetGroupIndexAction | SetAssetGroupEditAction; -export interface Domain { - type: string; - impactValue: number; - name: string; - id: string; - collected: boolean; -} - export interface SetExpandedAction { type: typeof GLOBAL_SET_EXPANDED; expanded: { [key: string]: symbol[] }; diff --git a/cmd/ui/src/views/Content.tsx b/cmd/ui/src/views/Content.tsx index 80c8976b8a..d4459e8634 100644 --- a/cmd/ui/src/views/Content.tsx +++ b/cmd/ui/src/views/Content.tsx @@ -61,7 +61,6 @@ const Content: React.FC = () => { } }, [authState, isFullyAuthenticated, dispatch]); - // set inital domain/tenant once user is authenticated useEffect(() => { if (isFullyAuthenticated) { const ctrl = new AbortController(); diff --git a/cmd/ui/src/views/Explore/GraphView.tsx b/cmd/ui/src/views/Explore/GraphView.tsx index 87ad7dc17a..371f23fb9f 100644 --- a/cmd/ui/src/views/Explore/GraphView.tsx +++ b/cmd/ui/src/views/Explore/GraphView.tsx @@ -34,6 +34,7 @@ import isEmpty from 'lodash/isEmpty'; import { FC, useEffect, useRef, useState } from 'react'; import { SigmaNodeEventPayload } from 'sigma/sigma'; import GraphButtons from 'src/components/GraphButtons/GraphButtons'; +import { NoDataDialogWithLinks } from 'src/components/NoDataDialogWithLinks'; import SigmaChart from 'src/components/SigmaChart'; import { setEntityInfoOpen, setSelectedNode } from 'src/ducks/entityinfo/actions'; import { GraphState } from 'src/ducks/explore/types'; @@ -48,7 +49,6 @@ import ExploreSearch from 'src/views/Explore/ExploreSearch'; import usePrompt from 'src/views/Explore/NavigationAlert'; import { initGraph } from 'src/views/Explore/utils'; import ContextMenu from './ContextMenu/ContextMenu'; -import { NoDataDialogWithLinks } from 'src/components/NoDataDialogWithLinks'; const columnsDefault = { xs: 6, md: 5, lg: 4, xl: 3 }; @@ -294,7 +294,7 @@ const GraphView: FC = () => { - + ); }; diff --git a/cmd/ui/src/views/QA/QA.tsx b/cmd/ui/src/views/QA/QA.tsx index 44834bce63..d7304dcaf4 100644 --- a/cmd/ui/src/views/QA/QA.tsx +++ b/cmd/ui/src/views/QA/QA.tsx @@ -20,6 +20,7 @@ import { ActiveDirectoryPlatformInfo, AzurePlatformInfo, DataSelector, + DataSelectorValueTypes, DomainInfo, PageWithTitle, TenantInfo, @@ -38,7 +39,7 @@ const useStyles = makeStyles((theme) => ({ const QualityAssurance: React.FC = () => { const domain = useAppSelector((state) => state.global.options.domain); - const [contextType, setContextType] = useState(domain?.type || null); + const [contextType, setContextType] = useState(domain?.type || null); const [contextId, setContextId] = useState(domain?.id || null); const [dataError, setDataError] = useState(false); const classes = useStyles(); diff --git a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx index f0ee197859..5ddeb947ff 100644 --- a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx +++ b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/GroupManagementContent.tsx @@ -22,7 +22,7 @@ import { AssetGroup, AssetGroupMember, AssetGroupMemberParams } from 'js-client- import { FC, ReactNode, useEffect, useState } from 'react'; import { useQuery } from 'react-query'; import { apiClient } from '../../utils'; -import DataSelector from '../../views/DataQuality/DataSelector'; +import {DataSelector} from '../../views/DataQuality/DataSelector'; import AssetGroupEdit from '../AssetGroupEdit'; import AssetGroupFilters from '../AssetGroupFilters'; import { FILTERABLE_PARAMS } from '../AssetGroupFilters/AssetGroupFilters'; diff --git a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts index f25ccc51c3..0e8f1c43fe 100644 --- a/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts +++ b/packages/javascript/bh-shared-ui/src/components/GroupManagementContent/types.ts @@ -14,7 +14,9 @@ // // SPDX-License-Identifier: Apache-2.0 +import { DataSelectorValueTypes } from '../../views/DataQuality/DataSelector/types'; + export type SelectedDomain = { id: string | null; - type: string | null; + type: DataSelectorValueTypes | null; }; diff --git a/packages/javascript/bh-shared-ui/src/hooks/useAvailableDomains.tsx b/packages/javascript/bh-shared-ui/src/hooks/useAvailableDomains.tsx index 785b60f9be..44fae4004d 100644 --- a/packages/javascript/bh-shared-ui/src/hooks/useAvailableDomains.tsx +++ b/packages/javascript/bh-shared-ui/src/hooks/useAvailableDomains.tsx @@ -14,10 +14,23 @@ // // SPDX-License-Identifier: Apache-2.0 -import { useQuery } from 'react-query'; +import { Domain } from 'js-client-library'; +import { useQuery, UseQueryOptions } from 'react-query'; import { apiClient } from '../utils/api'; -const useAvailableDomains = () => - useQuery('available-domains', () => apiClient.getAvailableDomains().then((response) => response.data.data)); +export const availableDomainKeys = { + all: ['available-domains'], +} as const; + +type QueryOptions = Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' +>; +const useAvailableDomains = (options?: QueryOptions) => + useQuery( + availableDomainKeys.all, + ({ signal }) => apiClient.getAvailableDomains({ signal }).then((response) => response.data.data), + options + ); export default useAvailableDomains; diff --git a/packages/javascript/bh-shared-ui/src/hooks/useDataQualityStats.tsx b/packages/javascript/bh-shared-ui/src/hooks/useDataQualityStats.tsx index f11f4cb70a..1c6329b5c5 100644 --- a/packages/javascript/bh-shared-ui/src/hooks/useDataQualityStats.tsx +++ b/packages/javascript/bh-shared-ui/src/hooks/useDataQualityStats.tsx @@ -18,14 +18,6 @@ import { DateTime } from 'luxon'; import { useQuery } from 'react-query'; import { apiClient } from '../utils/api'; -export type Domain = { - type: string; - impactValue: number; - name: string; - id: string; - collected: boolean; -}; - const now = DateTime.now(); export const useActiveDirectoryDataQualityHistoryQuery = (id: string) => { diff --git a/packages/javascript/bh-shared-ui/src/utils/types.ts b/packages/javascript/bh-shared-ui/src/utils/types.ts index 9d7e4d515b..cdefdb9c3d 100644 --- a/packages/javascript/bh-shared-ui/src/utils/types.ts +++ b/packages/javascript/bh-shared-ui/src/utils/types.ts @@ -24,3 +24,8 @@ export type DeepPartial = T extends object export type SortOrder = 'asc' | 'desc' | undefined; export type ValueOf = T[keyof T]; + +// [key in ] forces all options in string literal type to be in this map and nothing else +export type MappedStringLiteral = { + [key in T]: V; +}; 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 0935f9f483..b2f0af42d8 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 @@ -18,7 +18,7 @@ import userEvent from '@testing-library/user-event'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { render, screen, within } from '../../../test-utils'; -import DataSelector from './'; +import { DataSelector } from './'; const server = setupServer( rest.get(`/api/v2/available-domains`, (req, res, ctx) => { @@ -264,7 +264,7 @@ 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' }; + const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' } as const; render(); const contextSelector = await screen.findByTestId('data-quality_context-selector'); @@ -289,7 +289,7 @@ describe('Context Selector', () => { it('should initiate data loading when an item is selected', async () => { const user = userEvent.setup(); const testOnChange = vi.fn(); - const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' }; + const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' } as const; render(); const contextSelector = await screen.findByTestId('data-quality_context-selector'); @@ -338,7 +338,7 @@ describe('Context Selector', () => { it('should not render list items for domains that are not collected', async () => { const user = userEvent.setup(); const testOnChange = vi.fn(); - const testValue = { type: 'azure', id: 'd1993a1b-55c1-4668-9393-ddfffb6ab639' }; + const testValue = { type: 'azure', id: 'd1993a1b-55c1-4668-9393-ddfffb6ab639' } as const; render(); const contextSelector = await screen.findByTestId('data-quality_context-selector'); @@ -369,7 +369,7 @@ 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' }; + const testValue = { type: 'active-directory', id: '6b55e74d-f24e-418a-bfd1-4769e93517c7' } as const; render({testErrorMessage}} />); 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 868592dbbb..78f933f7c7 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 @@ -18,13 +18,15 @@ import { Button } from '@bloodhoundenterprise/doodleui'; import { faCloud, faGlobe } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Alert, Box, Divider, MenuItem, Popover, Skeleton, TextField, Tooltip, Typography } from '@mui/material'; +import { useAvailableDomains } from '../../../hooks'; import React, { ReactNode, useState } from 'react'; -import { Domain, useAvailableDomains } from '../../../hooks'; +import { Domain } from 'js-client-library'; +import { DataSelectorValueTypes } from './types'; const DataSelector: React.FC<{ - value: { type: string | null; id: string | null }; + value: { type: DataSelectorValueTypes | null; id: string | null }; errorMessage: ReactNode; - onChange?: (newValue: { type: string; id: string | null }) => void; + onChange?: (newValue: { type: DataSelectorValueTypes; id: string | null }) => void; fullWidth?: boolean; }> = ({ value, errorMessage, onChange = () => {}, fullWidth = false }) => { const [anchorEl, setAnchorEl] = useState(null); @@ -43,7 +45,7 @@ const DataSelector: React.FC<{ }; const open = Boolean(anchorEl); - const filteredDomains = data.filter((domain: Domain) => + const filteredDomains = data?.filter((domain: Domain) => domain.name.toLowerCase().includes(searchInput.toLowerCase()) ); diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/index.tsx b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/index.ts similarity index 88% rename from packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/index.tsx rename to packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/index.ts index 1c380ad6e8..a68147ff8b 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/index.tsx +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/index.ts @@ -14,6 +14,5 @@ // // SPDX-License-Identifier: Apache-2.0 -import DataSelector from './DataSelector'; - -export default DataSelector; +export { default as DataSelector } from './DataSelector'; +export * from './types'; diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/types.ts b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/types.ts new file mode 100644 index 0000000000..0b336dfdc8 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/DataSelector/types.ts @@ -0,0 +1,20 @@ +// Copyright 2024 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 { Domain } from 'js-client-library'; + +export type DomainPlatforms = 'active-directory-platform' | 'azure-platform'; +export type DataSelectorValueTypes = Domain['type'] | DomainPlatforms; diff --git a/packages/javascript/bh-shared-ui/src/views/DataQuality/index.ts b/packages/javascript/bh-shared-ui/src/views/DataQuality/index.ts index 986d3b3130..84c9877753 100644 --- a/packages/javascript/bh-shared-ui/src/views/DataQuality/index.ts +++ b/packages/javascript/bh-shared-ui/src/views/DataQuality/index.ts @@ -14,10 +14,10 @@ // // SPDX-License-Identifier: Apache-2.0 -export { default as DataSelector } from './DataSelector'; - export { default as LoadContainer } from './LoadContainer'; +export * from './DataSelector'; + export * from './DomainInfo'; export * from './TenantInfo'; diff --git a/packages/javascript/js-client-library/src/client.ts b/packages/javascript/js-client-library/src/client.ts index 969e10ce02..1c6c2aba6f 100644 --- a/packages/javascript/js-client-library/src/client.ts +++ b/packages/javascript/js-client-library/src/client.ts @@ -24,6 +24,7 @@ import { BasicResponse, CreateAuthTokenResponse, DatapipeStatusResponse, + Domain, EndFileIngestResponse, GetConfigurationResponse, ListAuthTokensResponse, @@ -106,7 +107,8 @@ class BHEAPIClient { return this.baseClient.post('/api/v2/clear-database', payload, options); }; - getAvailableDomains = (options?: types.RequestOptions) => this.baseClient.get('/api/v2/available-domains', options); + getAvailableDomains = (options?: types.RequestOptions) => + this.baseClient.get>('/api/v2/available-domains', options); /* audit */ getAuditLogs = (options?: types.RequestOptions) => this.baseClient.get('/api/v2/audit', options); @@ -279,47 +281,26 @@ class BHEAPIClient { ); }; - getPostureFindingTrends = ( - environmentId: string, - start?: Date, - end?: Date, - sort_by?: string, - options?: types.RequestOptions - ) => { - return this.baseClient.get( - `/api/v2/domains/${environmentId}/finding-trends`, - Object.assign( - { - params: { - start: start?.toISOString(), - end: end?.toISOString(), - sort_by, - }, - }, - options - ) - ); + getPostureFindingTrends = (environments: string[], start?: Date, end?: Date, options?: types.RequestOptions) => { + return this.baseClient.get(`/api/v2/attack-paths/finding-trends`, { + params: { environments, start: start?.toISOString(), end: end?.toISOString() }, + paramsSerializer: { indexes: null }, + ...options, + }); }; getPostureHistory = ( - environmentId: string, + environments: string[], dataType: string, start?: Date, end?: Date, options?: types.RequestOptions ) => { - return this.baseClient.get( - `/api/v2/domains/${environmentId}/posture-history/${dataType}`, - Object.assign( - { - params: { - start: start?.toISOString(), - end: end?.toISOString(), - }, - }, - options - ) - ); + return this.baseClient.get(`/api/v2/posture-history/${dataType}`, { + params: { environments, start: start?.toISOString(), end: end?.toISOString() }, + paramsSerializer: { indexes: null }, + ...options, + }); }; /* ingest */ diff --git a/packages/javascript/js-client-library/src/responses.ts b/packages/javascript/js-client-library/src/responses.ts index 136b1cab92..0b633553c7 100644 --- a/packages/javascript/js-client-library/src/responses.ts +++ b/packages/javascript/js-client-library/src/responses.ts @@ -41,6 +41,14 @@ type TimestampFields = { }; }; +export type Domain = { + type: 'active-directory' | 'azure'; + impactValue: number; + name: string; + id: string; + collected: boolean; +}; + export type ActiveDirectoryQualityStat = TimestampFields & { users: number; computers: number;