Skip to content

Commit

Permalink
BED-5133 (#1048)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
benwaples authored Jan 23, 2025
1 parent c1e0629 commit 3c60503
Show file tree
Hide file tree
Showing 17 changed files with 90 additions and 74 deletions.
3 changes: 2 additions & 1 deletion cmd/ui/src/ducks/global/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 1 addition & 8 deletions cmd/ui/src/ducks/global/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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[] };
Expand Down
1 change: 0 additions & 1 deletion cmd/ui/src/views/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions cmd/ui/src/views/Explore/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 };

Expand Down Expand Up @@ -294,7 +294,7 @@ const GraphView: FC = () => {
</Grid>
<ContextMenu contextMenu={contextMenu} handleClose={handleCloseContextMenu} />
<GraphProgress loading={graphState.loading} />
<NoDataDialogWithLinks open={!data.length} />
<NoDataDialogWithLinks open={!data?.length} />
</Box>
);
};
Expand Down
3 changes: 2 additions & 1 deletion cmd/ui/src/views/QA/QA.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ActiveDirectoryPlatformInfo,
AzurePlatformInfo,
DataSelector,
DataSelectorValueTypes,
DomainInfo,
PageWithTitle,
TenantInfo,
Expand All @@ -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<DataSelectorValueTypes | null>(domain?.type || null);
const [contextId, setContextId] = useState(domain?.id || null);
const [dataError, setDataError] = useState(false);
const classes = useStyles();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
19 changes: 16 additions & 3 deletions packages/javascript/bh-shared-ui/src/hooks/useAvailableDomains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown, unknown, Domain[], readonly ['available-domains']>,
'queryKey' | 'queryFn'
>;
const useAvailableDomains = (options?: QueryOptions) =>
useQuery(
availableDomainKeys.all,
({ signal }) => apiClient.getAvailableDomains({ signal }).then((response) => response.data.data),
options
);

export default useAvailableDomains;
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
5 changes: 5 additions & 0 deletions packages/javascript/bh-shared-ui/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ export type DeepPartial<T> = T extends object
export type SortOrder = 'asc' | 'desc' | undefined;

export type ValueOf<T> = T[keyof T];

// [key in <string literal>] forces all options in string literal type to be in this map and nothing else
export type MappedStringLiteral<T extends string | number, V = ''> = {
[key in T]: V;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(<DataSelector value={testValue} onChange={testOnChange} errorMessage={errorMessage} />);

const contextSelector = await screen.findByTestId('data-quality_context-selector');
Expand All @@ -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(<DataSelector value={testValue} onChange={testOnChange} errorMessage={errorMessage} />);

const contextSelector = await screen.findByTestId('data-quality_context-selector');
Expand Down Expand Up @@ -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(<DataSelector value={testValue} onChange={testOnChange} errorMessage={errorMessage} />);

const contextSelector = await screen.findByTestId('data-quality_context-selector');
Expand Down Expand Up @@ -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(<DataSelector value={testValue} onChange={testOnChange} errorMessage={<>{testErrorMessage}</>} />);

expect(await screen.findByText(testErrorMessage)).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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())
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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';
49 changes: 15 additions & 34 deletions packages/javascript/js-client-library/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
BasicResponse,
CreateAuthTokenResponse,
DatapipeStatusResponse,
Domain,
EndFileIngestResponse,
GetConfigurationResponse,
ListAuthTokensResponse,
Expand Down Expand Up @@ -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<BasicResponse<Domain[]>>('/api/v2/available-domains', options);

/* audit */
getAuditLogs = (options?: types.RequestOptions) => this.baseClient.get('/api/v2/audit', options);
Expand Down Expand Up @@ -279,47 +281,26 @@ class BHEAPIClient {
);
};

getPostureFindingTrends = (
environmentId: string,
start?: Date,
end?: Date,
sort_by?: string,
options?: types.RequestOptions
) => {
return this.baseClient.get<PostureFindingTrendsResponse>(
`/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<PostureFindingTrendsResponse>(`/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<PostureHistoryResponse>(
`/api/v2/domains/${environmentId}/posture-history/${dataType}`,
Object.assign(
{
params: {
start: start?.toISOString(),
end: end?.toISOString(),
},
},
options
)
);
return this.baseClient.get<PostureHistoryResponse>(`/api/v2/posture-history/${dataType}`, {
params: { environments, start: start?.toISOString(), end: end?.toISOString() },
paramsSerializer: { indexes: null },
...options,
});
};

/* ingest */
Expand Down
8 changes: 8 additions & 0 deletions packages/javascript/js-client-library/src/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 3c60503

Please sign in to comment.