Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/go/schemagen/generator/typescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ func GenerateTypeScriptActiveDirectory(root tsgen.File, schema model.ActiveDirec
GenerateTypeScriptStringEnum(root, "ActiveDirectoryKindProperties", schema.Properties)

GenerateTypeScriptPathfindingEdgesFn(root, "ActiveDirectoryPathfindingEdges", "ActiveDirectoryRelationshipKind", schema.PathfindingRelationships)
GenerateTypeScriptPathfindingEdgesFn(root, "ActiveDirectoryPathfindingEdgesMatchFrontend", "ActiveDirectoryRelationshipKind", schema.PathfindingRelationshipsMatchFrontend)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this list to TS schemagen output so that we can validate our hardcoded edge categories in the UI contain the correct edges.

}

func GenerateTypeScriptAzure(root tsgen.File, schema model.Azure) {
Expand Down
63 changes: 63 additions & 0 deletions packages/javascript/bh-shared-ui/src/graphSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,69 @@ export function ActiveDirectoryPathfindingEdges(): ActiveDirectoryRelationshipKi
ActiveDirectoryRelationshipKind.AbuseTGTDelegation,
];
}
export function ActiveDirectoryPathfindingEdgesMatchFrontend(): ActiveDirectoryRelationshipKind[] {
return [
ActiveDirectoryRelationshipKind.Owns,
ActiveDirectoryRelationshipKind.GenericAll,
ActiveDirectoryRelationshipKind.GenericWrite,
ActiveDirectoryRelationshipKind.WriteOwner,
ActiveDirectoryRelationshipKind.WriteDACL,
ActiveDirectoryRelationshipKind.MemberOf,
ActiveDirectoryRelationshipKind.ForceChangePassword,
ActiveDirectoryRelationshipKind.AllExtendedRights,
ActiveDirectoryRelationshipKind.AddMember,
ActiveDirectoryRelationshipKind.HasSession,
ActiveDirectoryRelationshipKind.GPLink,
ActiveDirectoryRelationshipKind.AllowedToDelegate,
ActiveDirectoryRelationshipKind.CoerceToTGT,
ActiveDirectoryRelationshipKind.AllowedToAct,
ActiveDirectoryRelationshipKind.AdminTo,
ActiveDirectoryRelationshipKind.CanPSRemote,
ActiveDirectoryRelationshipKind.CanRDP,
ActiveDirectoryRelationshipKind.ExecuteDCOM,
ActiveDirectoryRelationshipKind.HasSIDHistory,
ActiveDirectoryRelationshipKind.AddSelf,
ActiveDirectoryRelationshipKind.DCSync,
ActiveDirectoryRelationshipKind.ReadLAPSPassword,
ActiveDirectoryRelationshipKind.ReadGMSAPassword,
ActiveDirectoryRelationshipKind.DumpSMSAPassword,
ActiveDirectoryRelationshipKind.SQLAdmin,
ActiveDirectoryRelationshipKind.AddAllowedToAct,
ActiveDirectoryRelationshipKind.WriteSPN,
ActiveDirectoryRelationshipKind.AddKeyCredentialLink,
ActiveDirectoryRelationshipKind.SyncLAPSPassword,
ActiveDirectoryRelationshipKind.WriteAccountRestrictions,
ActiveDirectoryRelationshipKind.WriteGPLink,
ActiveDirectoryRelationshipKind.GoldenCert,
ActiveDirectoryRelationshipKind.ADCSESC1,
ActiveDirectoryRelationshipKind.ADCSESC3,
ActiveDirectoryRelationshipKind.ADCSESC4,
ActiveDirectoryRelationshipKind.ADCSESC6a,
ActiveDirectoryRelationshipKind.ADCSESC6b,
ActiveDirectoryRelationshipKind.ADCSESC9a,
ActiveDirectoryRelationshipKind.ADCSESC9b,
ActiveDirectoryRelationshipKind.ADCSESC10a,
ActiveDirectoryRelationshipKind.ADCSESC10b,
ActiveDirectoryRelationshipKind.ADCSESC13,
ActiveDirectoryRelationshipKind.SyncedToEntraUser,
ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToSMB,
ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToADCS,
ActiveDirectoryRelationshipKind.WriteOwnerLimitedRights,
ActiveDirectoryRelationshipKind.OwnsLimitedRights,
ActiveDirectoryRelationshipKind.ClaimSpecialIdentity,
ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToLDAP,
ActiveDirectoryRelationshipKind.CoerceAndRelayNTLMToLDAPS,
ActiveDirectoryRelationshipKind.HasTrustKeys,
ActiveDirectoryRelationshipKind.ManageCA,
ActiveDirectoryRelationshipKind.ManageCertificates,
ActiveDirectoryRelationshipKind.Contains,
ActiveDirectoryRelationshipKind.DCFor,
ActiveDirectoryRelationshipKind.SameForestTrust,
ActiveDirectoryRelationshipKind.SpoofSIDHistory,
ActiveDirectoryRelationshipKind.AbuseTGTDelegation,
ActiveDirectoryRelationshipKind.ProtectAdminGroups,
];
}
export enum AzureNodeKind {
Entity = 'AZBase',
VMScaleSet = 'AZVMScaleSet',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,23 @@
import { apiClient } from '../../../utils';
import { ExploreQueryParams } from '../../useExploreParams';
import {
areFiltersEmpty,
createPathFilterString,
ExploreGraphQuery,
ExploreGraphQueryError,
ExploreGraphQueryKey,
ExploreGraphQueryOptions,
INITIAL_FILTER_TYPES,
sharedGraphQueryOptions,
} from './utils';

// Only need to create our default filters once
const DEFAULT_FILTERS = createPathFilterString(INITIAL_FILTER_TYPES);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default filters are no longer needed due to the addition of the only_traversable query param that we now use to automatically filter out non-traversable edges.


export const pathfindingSearchGraphQuery = (paramOptions: Partial<ExploreQueryParams>): ExploreGraphQueryOptions => {
const { searchType, primarySearch, secondarySearch, pathFilters } = paramOptions;

// Query should occur whether or not pathFilters exist
if (!primarySearch || !searchType || !secondarySearch) {
if (!primarySearch || !searchType || !secondarySearch || areFiltersEmpty(pathFilters)) {
return { enabled: false };
}

const filter = pathFilters?.length ? createPathFilterString(pathFilters) : DEFAULT_FILTERS;
const filter = pathFilters?.length ? createPathFilterString(pathFilters) : undefined;

return {
...sharedGraphQueryOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

import { FlatGraphResponse, GraphResponse, StyledGraphEdge, StyledGraphNode, type GraphData } from 'js-client-library';
import { UseQueryOptions } from 'react-query';
import { BUILTIN_EDGE_CATEGORIES } from '../../../views/Explore/ExploreSearch/EdgeFilter/edgeCategories';
import { ExploreQueryParams } from '../../useExploreParams';
import { extractEdgeTypes, getInitialPathFilters } from '../utils';
import { getInitialPathFilters } from '../utils';

type QueryKeys = ('explore-graph-query' | string | undefined)[];

Expand All @@ -39,20 +40,21 @@ export type ExploreGraphQuery = {

export const ExploreGraphQueryKey = 'explore-graph-query';

export const INITIAL_FILTERS = getInitialPathFilters();
export const INITIAL_FILTER_TYPES = extractEdgeTypes(INITIAL_FILTERS);
export const INITIAL_FILTERS = getInitialPathFilters(BUILTIN_EDGE_CATEGORIES);
export const EMPTY_FILTER_VALUE = 'empty';

export const sharedGraphQueryOptions: ExploreGraphQueryOptions = {
retry: false,
refetchOnWindowFocus: false,
};

// creates a filter string in our API format, handling the case that our 'empty' value is in the url param
// Checks if a list of path filters consists of the string 'empty', which indicates all filters are unchecked
export const areFiltersEmpty = (types: string[] | null | undefined) => {
return !!(types && types[0] === EMPTY_FILTER_VALUE);
};

// Creates an inclusive filter string formatted for the API from a list of edge types
export const createPathFilterString = (types: string[]) => {
if (types[0] === EMPTY_FILTER_VALUE) {
return `nin:${INITIAL_FILTER_TYPES.join(',')}`;
}
return `in:${types.join(',')}`;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ import { extractEdgeTypes } from './utils';
const TEST_FILTER = INITIAL_FILTERS[0];

describe('usePathfindingFilters', () => {
it('initializes the list with all filters checked by default', () => {
it('initializes the list with all filters checked by default', async () => {
const hook = renderHook(() => usePathfindingFilters());

await act(() => hook.result.current.initialize());

expect(hook.result.current.selectedFilters).toEqual(INITIAL_FILTERS);
});

it('will update the selected filters based on the values stored in query params when the initialize function is called', async () => {
const url = `?pathFilters=${TEST_FILTER.edgeType}`;
const hook = renderHook(() => usePathfindingFilters(), { route: url });

expect(hook.result.current.selectedFilters).toEqual(INITIAL_FILTERS);

await act(() => hook.result.current.initialize());

const edgeTypesInFilter = extractEdgeTypes(hook.result.current.selectedFilters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@
// SPDX-License-Identifier: Apache-2.0

import { useState } from 'react';
import { EdgeCheckboxType } from '../../edgeTypes';
import { areArraysSimilar } from '../../utils';
import { EdgeCheckboxType } from '../../views/Explore/ExploreSearch/EdgeFilter/edgeCategories';
import { useEdgeCategories } from '../../views/Explore/ExploreSearch/EdgeFilter/useEdgeCategories';
import { useExploreParams } from '../useExploreParams';
import { EMPTY_FILTER_VALUE, INITIAL_FILTERS, INITIAL_FILTER_TYPES } from './queries';
import { extractEdgeTypes, mapParamsToFilters } from './utils';
import { EMPTY_FILTER_VALUE } from './queries';
import { extractEdgeTypes, getInitialPathFilters, mapParamsToFilters } from './utils';

export const usePathfindingFilters = () => {
const [selectedFilters, updateSelectedFilters] = useState<EdgeCheckboxType[]>(INITIAL_FILTERS);
const [selectedFilters, updateSelectedFilters] = useState<EdgeCheckboxType[]>([]);
const { pathFilters, setExploreParams } = useExploreParams();
const { edgeCategories, isLoading } = useEdgeCategories();

const filters = getInitialPathFilters(edgeCategories);
const types = extractEdgeTypes(filters);

// Instead of tracking this in an effect, we want to create a callback to let the consumer decide when to sync down
// query params. This is useful for our filter form where we only want to sync once when the user opens it
Expand All @@ -32,10 +37,10 @@ export const usePathfindingFilters = () => {
// Since we need to track state in the case of an empty set of filters, check for our 'empty' key here
const incoming = pathFilters[0] === EMPTY_FILTER_VALUE ? [] : pathFilters;

const mapped = mapParamsToFilters(incoming, INITIAL_FILTERS);
const mapped = mapParamsToFilters(incoming, filters);
updateSelectedFilters(mapped);
} else {
updateSelectedFilters(INITIAL_FILTERS);
updateSelectedFilters(filters);
}
};

Expand All @@ -47,7 +52,7 @@ export const usePathfindingFilters = () => {
if (selectedEdgeTypes.length === 0) {
// query string stores a value indicating an empty set if every option is unselected
setExploreParams({ pathFilters: [EMPTY_FILTER_VALUE] });
} else if (areArraysSimilar(INITIAL_FILTER_TYPES, selectedEdgeTypes)) {
} else if (areArraysSimilar(types, selectedEdgeTypes)) {
// query string is not set if user selects the default
setExploreParams({ pathFilters: null });
} else {
Expand All @@ -60,5 +65,6 @@ export const usePathfindingFilters = () => {
initialize,
handleApplyFilters,
handleUpdateFilters,
isLoading,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//
// SPDX-License-Identifier: Apache-2.0

import { AllEdgeTypes, Category, EdgeCheckboxType, Subcategory } from '../../edgeTypes';
import { Category, EdgeCheckboxType, Subcategory } from '../../views/Explore/ExploreSearch/EdgeFilter/edgeCategories';

export const extractEdgeTypes = (edges: EdgeCheckboxType[]): string[] => {
return edges.filter((edge) => edge.checked).map((edge) => edge.edgeType);
Expand All @@ -28,10 +28,10 @@ export const mapParamsToFilters = (params: string[], initial: EdgeCheckboxType[]
};

// Create a list of all edge types to initialize pathfinding filter state
export const getInitialPathFilters = (): EdgeCheckboxType[] => {
export const getInitialPathFilters = (categories: Category[]): EdgeCheckboxType[] => {
const initialPathFilters: EdgeCheckboxType[] = [];

AllEdgeTypes.forEach((category: Category) => {
categories.forEach((category: Category) => {
category.subcategories.forEach((subcategory: Subcategory) => {
subcategory.edgeTypes.forEach((edgeType: string) => {
initialPathFilters.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
// SPDX-License-Identifier: Apache-2.0
import { useCallback } from 'react';
import { NavigateOptions, useSearchParams } from 'react-router-dom';
import { EdgeCheckboxType } from '../../edgeTypes';
import { MappedStringLiteral } from '../../types';
import { EntityRelationshipQueryTypes, entityRelationshipEndpoints } from '../../utils/content';
import { setParamsFactory } from '../../utils/searchParams/searchParams';
import { EdgeCheckboxType } from '../../views/Explore/ExploreSearch/EdgeFilter/edgeCategories';

export type ExploreSearchTab = 'node' | 'pathfinding' | 'cypher';
type SearchType = ExploreSearchTab | 'relationship' | 'composition' | 'aclinheritance';
Expand Down
1 change: 0 additions & 1 deletion packages/javascript/bh-shared-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ declare module '@mui/material/IconButton' {

export * from './components';
export * from './constants';
export * from './edgeTypes';
export * from './graphSchema';
export * from './hooks';
export * from './mocks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { INHERITANCE_DROPDOWN_DESCRIPTION } from '../../../components/HelpTexts/shared/ACLInheritance';
import { SelectedEdge } from '../../../edgeTypes';
import {
ActiveDirectoryKindProperties,
ActiveDirectoryNodeKind,
ActiveDirectoryRelationshipKind,
CommonKindProperties,
} from '../../../graphSchema';
import { render, screen, waitFor } from '../../../test-utils';
import { SelectedEdge } from '../ExploreSearch/EdgeFilter/edgeCategories';
import { ObjectInfoPanelContextProvider } from '../providers';
import EdgeInfoContent from './EdgeInfoContent';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { Box, Divider, Typography } from '@mui/material';
import { ElementType, FC, Fragment } from 'react';
import EdgeInfoComponents from '../../../components/HelpTexts';
import ACLInheritance from '../../../components/HelpTexts/shared/ACLInheritance';
import { EdgeSections, SelectedEdge } from '../../../edgeTypes';
import { ActiveDirectoryKindProperties, CommonKindProperties } from '../../../graphSchema';
import { useExploreParams, useFetchEntityInfo } from '../../../hooks';
import { EdgeSections, SelectedEdge } from '../ExploreSearch/EdgeFilter/edgeCategories';
import { FieldsContainer } from '../fragments';
import EdgeInfoCollapsibleSection from './EdgeInfoCollapsibleSection';
import EdgeObjectInformation from './EdgeObjectInformation';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import { Badge } from '@bloodhoundenterprise/doodleui';
import { faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { HTMLProps } from 'react';
import { SelectedEdge } from '../../../edgeTypes';
import useRoleBasedFiltering from '../../../hooks/useRoleBasedFiltering';
import { cn } from '../../../utils';
import { SelectedEdge } from '../ExploreSearch/EdgeFilter/edgeCategories';
import { ObjectInfoPanelContextProvider } from '../providers';
import EdgeInfoContent from './EdgeInfoContent';
import Header from './EdgeInfoHeader';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
// SPDX-License-Identifier: Apache-2.0
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { SelectedEdge } from '../../../edgeTypes';
import { render, screen } from '../../../test-utils';
import { SelectedEdge } from '../ExploreSearch/EdgeFilter/edgeCategories';
import { ObjectInfoPanelContextProvider } from '../providers';
import EdgeObjectInformation from './EdgeObjectInformation';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
import { Skeleton } from '@mui/material';
import { FC, useEffect } from 'react';
import { useQuery } from 'react-query';
import { SelectedEdge } from '../../../edgeTypes';
import { usePreviousValue } from '../../../hooks';
import { EntityField, apiClient, formatObjectInfoFields } from '../../../utils';
import { SelectedEdge } from '../ExploreSearch/EdgeFilter/edgeCategories';
import { FieldsContainer, ObjectInfoFields } from '../fragments';
import { useObjectInfoPanelContext } from '../providers';
import EdgeInfoCollapsibleSection from './EdgeInfoCollapsibleSection';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { usePathfindingFilters } from '../../../hooks';
import { act, render, screen } from '../../../test-utils';
import { usePathfindingFilters } from '../../../../hooks/useExploreGraph/usePathfindingFilters';
import { act, render, screen } from '../../../../test-utils';
import { EdgeFilter } from './EdgeFilter';

const server = setupServer(
Expand Down Expand Up @@ -78,8 +78,8 @@ describe('EdgeFilter', () => {
const pathfindingButton = screen.getByRole('button', { name: /filter/i });
await user.click(pathfindingButton);

const cancelButton = screen.getByRole('button', { name: /apply/i });
await user.click(cancelButton);
const applyButton = screen.getByRole('button', { name: /apply/i });
await user.click(applyButton);

const dialog = screen.getByRole('dialog', { name: /path edge filtering/i });
expect(dialog).not.toBeVisible();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { Button } from '@bloodhoundenterprise/doodleui';
import { faFilter } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState } from 'react';
import { EdgeCheckboxType } from '../../..';
import EdgeFilteringDialog from './EdgeFilteringDialog';
import { EdgeCheckboxType } from './edgeCategories';

export type PathfindingFilterState = {
selectedFilters: EdgeCheckboxType[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,18 @@

import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { AllEdgeTypes, EdgeCheckboxType } from '../../../edgeTypes';
import { getInitialPathFilters } from '../../../hooks';
import { act, render, screen } from '../../../test-utils';
import { INITIAL_FILTERS } from '../../../../hooks/useExploreGraph/queries';
import { act, render, screen } from '../../../../test-utils';
import EdgeFilteringDialog from './EdgeFilteringDialog';
import { BUILTIN_EDGE_CATEGORIES, EdgeCheckboxType } from './edgeCategories';

const INITIAL_FILTERS = getInitialPathFilters();
const WrappedDialog = () => {
const [selectedFilters, setSelectedFilters] = useState<EdgeCheckboxType[]>(INITIAL_FILTERS);
return (
<EdgeFilteringDialog
isOpen
selectedFilters={selectedFilters}
handleCancel={vi.fn}
handleCancel={vi.fn()}
handleApply={vi.fn()}
handleUpdate={(filters) => setSelectedFilters(filters)}
/>
Expand Down Expand Up @@ -69,7 +68,7 @@ describe('Pathfinding', () => {
await user.click(expandSubcategoryButton);

// assert all subcategories underneath `Active Directory` category are `CHECKED`
const activeDirectorySubcategories = AllEdgeTypes[0].subcategories;
const activeDirectorySubcategories = BUILTIN_EDGE_CATEGORIES[0].subcategories;
activeDirectorySubcategories.forEach((subcategory) => {
const subcategoryElement = screen.getByRole('checkbox', { name: subcategory.name });
expect(subcategoryElement).toBeChecked();
Expand Down Expand Up @@ -110,7 +109,7 @@ describe('Pathfinding', () => {
// assert that subcategory and all children are checked
const subcategoryCheckbox = screen.getByRole('checkbox', { name: 'Active Directory Structure' });
expect(subcategoryCheckbox).toBeChecked();
const edgeTypes = AllEdgeTypes[0].subcategories[0].edgeTypes;
const edgeTypes = BUILTIN_EDGE_CATEGORIES[0].subcategories[0].edgeTypes;
edgeTypes.forEach((edgeType) => {
const edgeTypeCheckbox = screen.getByRole('checkbox', { name: edgeType });
expect(edgeTypeCheckbox).toBeChecked();
Expand Down
Loading