[Security Solution] Display security job friendly name on Explore and Investigation pages (#148780)

issue: https://github.com/elastic/kibana/issues/146772

## Summary
To help our users understand what pre-built jobs do, we will display a
human-readable job name inside security solutions when available on the
job configuration. The job name is configured inside
`job.custom_settings.security_app_display_name`. This change will affect
the following pages:

- Entity analytics page: Notable anomalies sections
- User page: Anomalies tab - job filter, and job column
- Host page: Anomalies tab - job filter, and job column
- Network page: Anomalies tab - job filter, and job column
- User details page and flyout: Anomaly popover (it shows when you click
on the information icon)
- Host details page and flyout: Anomaly popover (it shows when you click
on the information icon)
- Network details page and flyout: Anomaly popover (it shows when you
click on the information icon)
- ML Job Setting panel: Available on the Alerts page

**The User/Host/Network flyout is displayed in Alerts and Timeline**

### Not included (follow-up PR)
* Rules details page
* Rules creation page

### Before

<img width="600" alt="Screenshot 2023-01-12 at 10 08 32"
src="https://user-images.githubusercontent.com/1490444/212025037-d2bec806-3439-4758-b01c-532957faff2b.png">
<img width="600" alt="Screenshot 2023-01-12 at 10 07 55"
src="https://user-images.githubusercontent.com/1490444/212025045-2892b2e1-290c-459a-bb39-cf40033ad334.png">
<img width="600" alt="Screenshot 2023-01-12 at 10 06 59"
src="https://user-images.githubusercontent.com/1490444/212025057-f2fe612f-5718-42b7-9e47-b7bacb513539.png">
<img width="600" alt="Screenshot 2023-01-12 at 10 10 54"
src="https://user-images.githubusercontent.com/1490444/212025877-b4a0698c-d716-47b9-8095-72ca2ea19064.png">

### After

<img width="600" alt="Screenshot 2023-01-12 at 14 34 25"
src="https://user-images.githubusercontent.com/1490444/212096616-0f144a08-482e-4ab6-a0ea-88198d199531.png">
<img width="600" alt="Screenshot 2023-01-12 at 14 34 47"
src="https://user-images.githubusercontent.com/1490444/212096627-92dce2aa-1ea0-409b-bcd2-ce13a7031288.png">
<img width="600" alt="Screenshot 2023-01-12 at 14 37 06"
src="https://user-images.githubusercontent.com/1490444/212096634-6f23ed16-4995-4764-9558-25028a6376bc.png">
<img width="600" alt="Screenshot 2023-01-12 at 15 44 53"
src="https://user-images.githubusercontent.com/1490444/212097151-be2f6198-7f56-4a78-8553-eb25e65909cf.png">

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Pablo Machado 2023-01-18 18:15:42 +01:00 committed by GitHub
parent 150b447c9d
commit f1934a3a27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 579 additions and 199 deletions

View file

@ -5,14 +5,15 @@
* 2.0.
*/
import React from 'react';
import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs';
import React, { useMemo } from 'react';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
import type { InfluencerInput, Anomalies, CriteriaFields } from '../types';
import { useAnomaliesTableData } from './use_anomalies_table_data';
interface ChildrenArgs {
isLoadingAnomaliesData: boolean;
anomaliesData: Anomalies | null;
jobNameById: Record<string, string | undefined>;
}
interface Props {
@ -26,7 +27,9 @@ interface Props {
export const AnomalyTableProvider = React.memo<Props>(
({ influencers, startDate, endDate, children, criteriaFields, skip }) => {
const { jobIds } = useInstalledSecurityJobsIds();
const { jobNameById } = useInstalledSecurityJobNameById();
const jobIds = useMemo(() => Object.keys(jobNameById), [jobNameById]);
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
criteriaFields,
influencers,
@ -36,7 +39,7 @@ export const AnomalyTableProvider = React.memo<Props>(
jobIds,
aggregationInterval: 'auto',
});
return <>{children({ isLoadingAnomaliesData, anomaliesData })}</>;
return <>{children({ isLoadingAnomaliesData, anomaliesData, jobNameById })}</>;
}
);

View file

@ -50,7 +50,7 @@ describe('useNotableAnomaliesSearch', () => {
wrapper: TestProviders,
});
expect(result.current.data.length).toEqual(6);
expect(result.current.data.length).toEqual(0);
});
it('calls notableAnomaliesSearch when skip is false', async () => {
@ -116,6 +116,63 @@ describe('useNotableAnomaliesSearch', () => {
});
});
it('returns jobs sorted by name', async () => {
await act(async () => {
const firstJobId = 'v3_windows_anomalous_script';
const secondJobId = 'auth_rare_source_ip_for_a_user';
const fistJobCount = { key: firstJobId, doc_count: 99 };
const secondJobCount = { key: secondJobId, doc_count: 99 };
const firstJobSecurityName = '0000001';
const secondJobSecurityName = '0000002';
const firstJob = {
id: firstJobId,
jobState: 'started',
datafeedState: 'started',
customSettings: {
security_app_display_name: firstJobSecurityName,
},
};
const secondJob = {
id: secondJobId,
jobState: 'started',
datafeedState: 'started',
customSettings: {
security_app_display_name: secondJobSecurityName,
},
};
mockNotableAnomaliesSearch.mockResolvedValue({
aggregations: { number_of_anomalies: { buckets: [fistJobCount, secondJobCount] } },
});
mockUseSecurityJobs.mockReturnValue({
loading: false,
isMlAdmin: true,
jobs: [firstJob, secondJob],
refetch: useSecurityJobsRefetch,
});
const { result, waitForNextUpdate } = renderHook(
() => useNotableAnomaliesSearch({ skip: false, from, to }),
{
wrapper: TestProviders,
}
);
await waitForNextUpdate();
await waitForNextUpdate();
const names = result.current.data.map(({ name }) => name);
expect(names).toEqual([
firstJobSecurityName,
secondJobSecurityName,
'packetbeat_dns_tunneling',
'packetbeat_rare_dns_question',
'packetbeat_rare_server_domain',
'suspicious_login_activity',
]);
});
});
it('does not throw error when aggregations is undefined', async () => {
await act(async () => {
mockNotableAnomaliesSearch.mockResolvedValue({});
@ -132,40 +189,6 @@ describe('useNotableAnomaliesSearch', () => {
});
});
it('returns uninstalled jobs', async () => {
mockUseSecurityJobs.mockReturnValue({
loading: false,
isMlAdmin: true,
jobs: [],
refetch: useSecurityJobsRefetch,
});
await act(async () => {
mockNotableAnomaliesSearch.mockResolvedValue({
aggregations: { number_of_anomalies: { buckets: [] } },
});
const { result, waitForNextUpdate } = renderHook(
() => useNotableAnomaliesSearch({ skip: false, from, to }),
{
wrapper: TestProviders,
}
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current.data).toEqual(
expect.arrayContaining([
{
count: 0,
name: job.id,
job: undefined,
entity: AnomalyEntity.Host,
},
])
);
});
});
it('returns jobs with custom job ids', async () => {
const customJobId = `test_${jobId}`;
const jobCount = { key: customJobId, doc_count: 99 };

View file

@ -6,7 +6,7 @@
*/
import { useState, useEffect, useMemo } from 'react';
import { filter, head, orderBy, pipe, has } from 'lodash/fp';
import { filter, head, orderBy, pipe, has, sortBy } from 'lodash/fp';
import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants';
import * as i18n from './translations';
@ -26,7 +26,7 @@ export enum AnomalyEntity {
}
export interface AnomaliesCount {
name: NotableAnomaliesJobId;
name: NotableAnomaliesJobId | string;
count: number;
entity: AnomalyEntity;
job?: SecurityJob;
@ -47,7 +47,7 @@ export const useNotableAnomaliesSearch = ({
data: AnomaliesCount[];
refetch: inputsModel.Refetch;
} => {
const [data, setData] = useState<AnomaliesCount[]>(formatResultData([], []));
const [data, setData] = useState<AnomaliesCount[]>([]);
const {
loading: jobsLoading,
@ -131,18 +131,20 @@ function formatResultData(
}>,
notableAnomaliesJobs: SecurityJob[]
): AnomaliesCount[] {
return NOTABLE_ANOMALIES_IDS.map((notableJobId) => {
const unsortedAnomalies: AnomaliesCount[] = NOTABLE_ANOMALIES_IDS.map((notableJobId) => {
const job = findJobWithId(notableJobId)(notableAnomaliesJobs);
const bucket = buckets.find(({ key }) => key === job?.id);
const hasUserName = has("entity.hits.hits[0]._source['user.name']", bucket);
return {
name: notableJobId,
name: job?.customSettings?.security_app_display_name ?? notableJobId,
count: bucket?.doc_count ?? 0,
entity: hasUserName ? AnomalyEntity.User : AnomalyEntity.Host,
job,
};
});
return sortBy(['name'], unsortedAnomalies);
}
/**

View file

@ -72,3 +72,18 @@ export const useInstalledSecurityJobsIds = () => {
return { jobIds, loading };
};
export const useInstalledSecurityJobNameById = () => {
const { jobs, loading } = useInstalledSecurityJobs();
const jobNameById = useMemo(
() =>
jobs.reduce<Record<string, string | undefined>>((acc, job) => {
acc[job.id] = job.customSettings?.security_app_display_name;
return acc;
}, {}),
[jobs]
);
return { jobNameById, loading };
};

View file

@ -48,7 +48,7 @@ export const ExplorerLink: React.FC<ExplorerLinkProps> = ({
if (!explorerUrl) return null;
return (
<EuiLink href={explorerUrl} target="_blank">
<EuiLink href={explorerUrl} target="_blank" data-test-subj={`explorer-link-${score.jobId}`}>
{linkName}
</EuiLink>
);

View file

@ -11,6 +11,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = `
index={0}
interval="day"
jobKey="job-1-16.193669439507826-process.name-du"
jobName="job-1"
key="job-1-16.193669439507826-process.name-du"
narrowDateRange={[MockFunction]}
score={
@ -86,6 +87,7 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = `
index={1}
interval="day"
jobKey="job-2-16.193669439507826-process.name-ls"
jobName="job-2"
key="job-2-16.193669439507826-process.name-ls"
narrowDateRange={[MockFunction]}
score={

View file

@ -40,6 +40,7 @@ describe('anomaly_scores', () => {
score={anomalies.anomalies[0]}
interval="day"
narrowDateRange={narrowDateRange}
jobName={'job-1'}
/>
);
expect(wrapper).toMatchSnapshot();
@ -55,6 +56,7 @@ describe('anomaly_scores', () => {
score={anomalies.anomalies[0]}
interval="day"
narrowDateRange={narrowDateRange}
jobName={'job-1'}
/>
</TestProviders>
);
@ -71,6 +73,7 @@ describe('anomaly_scores', () => {
score={anomalies.anomalies[0]}
interval="day"
narrowDateRange={narrowDateRange}
jobName={'job-1'}
/>
</TestProviders>
);

View file

@ -21,6 +21,7 @@ interface Args {
index?: number;
score: Anomaly;
interval: string;
jobName: string;
}
const Icon = styled(EuiIcon)`
@ -38,6 +39,7 @@ export const AnomalyScoreComponent = ({
score,
interval,
narrowDateRange,
jobName,
}: Args): JSX.Element => {
const [isOpen, setIsOpen] = useState(false);
return (
@ -61,7 +63,14 @@ export const AnomalyScoreComponent = ({
>
<EuiDescriptionList
data-test-subj="anomaly-description-list"
listItems={createDescriptionList(score, startDate, endDate, interval, narrowDateRange)}
listItems={createDescriptionList(
score,
startDate,
endDate,
interval,
narrowDateRange,
jobName
)}
/>
</EuiPopover>
</EuiFlexItem>

View file

@ -39,6 +39,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={false}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
);
expect(wrapper).toMatchSnapshot();
@ -53,6 +54,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={true}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
</TestProviders>
);
@ -68,6 +70,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={false}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
</TestProviders>
);
@ -83,6 +86,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={false}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
</TestProviders>
);
@ -99,6 +103,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={false}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
</TestProviders>
);
@ -119,6 +124,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={false}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
</TestProviders>
);
@ -134,6 +140,7 @@ describe('anomaly_scores', () => {
endDate={endDate}
isLoading={false}
narrowDateRange={narrowDateRange}
jobNameById={{}}
/>
</TestProviders>
);

View file

@ -19,6 +19,7 @@ interface Args {
isLoading: boolean;
narrowDateRange: NarrowDateRange;
limit?: number;
jobNameById: Record<string, string | undefined>;
}
export const createJobKey = (score: Anomaly): string =>
@ -31,6 +32,7 @@ export const AnomalyScoresComponent = ({
isLoading,
narrowDateRange,
limit,
jobNameById,
}: Args): JSX.Element => {
if (isLoading) {
return <EuiLoadingSpinner data-test-subj="anomaly-score-spinner" size="m" />;
@ -50,6 +52,7 @@ export const AnomalyScoresComponent = ({
endDate={endDate}
index={index}
score={score}
jobName={jobNameById[score.jobId] ?? score.jobId}
interval={anomalies.interval}
narrowDateRange={narrowDateRange}
/>

View file

@ -29,7 +29,8 @@ export const createDescriptionList = (
startDate: string,
endDate: string,
interval: string,
narrowDateRange: NarrowDateRange
narrowDateRange: NarrowDateRange,
jobName: string
): DescriptionList[] => {
const descriptionList: DescriptionList[] = [
{
@ -50,7 +51,7 @@ export const createDescriptionList = (
),
description: (
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
<EuiFlexItem grow={false}>{score.jobId}</EuiFlexItem>
<EuiFlexItem grow={false}>{jobName}</EuiFlexItem>
<EuiFlexItem grow={false}>
<ExplorerLink
score={score}

View file

@ -33,7 +33,8 @@ describe('create_description_list', () => {
startDate,
endDate,
'hours',
narrowDateRange
narrowDateRange,
'job-1'
)}
/>
);
@ -48,7 +49,8 @@ describe('create_description_list', () => {
startDate,
endDate,
'hours',
narrowDateRange
narrowDateRange,
'job-1'
)}
/>
);
@ -69,7 +71,8 @@ describe('create_description_list', () => {
startDate,
endDate,
'hours',
narrowDateRange
narrowDateRange,
'job-1'
)}
/>
);
@ -129,7 +132,8 @@ describe('create_description_list', () => {
startDate,
endDate,
'hours',
narrowDateRange
narrowDateRange,
'job-1'
)}
/>
);

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { mount } from 'enzyme';
import { AnomaliesHostTable } from './anomalies_host_table';
import { TestProviders } from '../../../mock';
import React from 'react';
@ -13,27 +12,66 @@ import { useQueryToggle } from '../../../containers/query_toggle';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HostsType } from '../../../../explore/hosts/store/model';
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
import { fireEvent, render } from '@testing-library/react';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
import { mockAnomalies } from '../mock';
import { useMlHref } from '@kbn/ml-plugin/public';
jest.mock('../../../containers/query_toggle');
jest.mock('../anomaly/use_anomalies_table_data');
jest.mock('../../../../../common/machine_learning/has_ml_user_permissions');
jest.mock('../hooks/use_installed_security_jobs');
jest.mock('@kbn/ml-plugin/public');
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock;
const mockUseInstalledSecurityJobNameById = useInstalledSecurityJobNameById as jest.Mock;
const mockUseMlHref = useMlHref as jest.Mock;
const mockSetToggle = jest.fn();
(hasMlUserPermissions as jest.Mock).mockReturnValue(true);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseMlHref.mockReturnValue('http://test');
mockUseInstalledSecurityJobNameById.mockReturnValue({
loading: false,
jobNameById: {},
});
const testProps = {
startDate: '2019-07-17T20:00:00.000Z',
endDate: '2019-07-18T20:00:00.000Z',
narrowDateRange: jest.fn(),
skip: false,
type: HostsType.page,
};
describe('Anomalies host table', () => {
it('renders job name when available', () => {
const anomaly = mockAnomalies.anomalies[0];
const jobName = 'job_name';
mockUseAnomaliesTableData.mockReturnValue([
false,
{
anomalies: [anomaly],
interval: '10',
},
]);
mockUseInstalledSecurityJobNameById.mockReturnValue({
loading: false,
jobNameById: { [anomaly.jobId]: jobName },
});
const { getByTestId } = render(<AnomaliesHostTable {...testProps} />, {
wrapper: TestProviders,
});
expect(getByTestId(`explorer-link-${anomaly.jobId}`).textContent).toContain(jobName);
});
describe('toggle query', () => {
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock;
const mockSetToggle = jest.fn();
const testProps = {
startDate: '2019-07-17T20:00:00.000Z',
endDate: '2019-07-18T20:00:00.000Z',
narrowDateRange: jest.fn(),
skip: false,
type: HostsType.page,
};
beforeEach(() => {
jest.clearAllMocks();
(hasMlUserPermissions as jest.Mock).mockReturnValue(true);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseAnomaliesTableData.mockReturnValue([
false,
{
@ -44,42 +82,43 @@ describe('Anomalies host table', () => {
});
test('toggleQuery updates toggleStatus', () => {
const wrapper = mount(<AnomaliesHostTable {...testProps} />, {
wrappingComponent: TestProviders,
const { getByTestId } = render(<AnomaliesHostTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false);
wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click');
fireEvent.click(getByTestId('query-toggle-header'));
expect(mockSetToggle).toBeCalledWith(false);
expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true);
});
test('toggleStatus=true, do not skip', () => {
mount(<AnomaliesHostTable {...testProps} />, {
wrappingComponent: TestProviders,
render(<AnomaliesHostTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false);
});
test('toggleStatus=true, render components', () => {
const wrapper = mount(<AnomaliesHostTable {...testProps} />, {
wrappingComponent: TestProviders,
const { queryByTestId } = render(<AnomaliesHostTable {...testProps} />, {
wrapper: TestProviders,
});
expect(wrapper.find('[data-test-subj="host-anomalies-table"]').exists()).toBe(true);
expect(queryByTestId('host-anomalies-table')).toBeInTheDocument();
});
test('toggleStatus=false, do not render components', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
const wrapper = mount(<AnomaliesHostTable {...testProps} />, {
wrappingComponent: TestProviders,
const { queryByTestId } = render(<AnomaliesHostTable {...testProps} />, {
wrapper: TestProviders,
});
expect(wrapper.find('[data-test-subj="host-anomalies-table"]').exists()).toBe(false);
expect(queryByTestId('host-anomalies-table')).not.toBeInTheDocument();
});
test('toggleStatus=false, skip', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
mount(<AnomaliesHostTable {...testProps} />, {
wrappingComponent: TestProviders,
render(<AnomaliesHostTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true);

View file

@ -22,7 +22,7 @@ import { BasicTable } from './basic_table';
import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type';
import { Panel } from '../../panel';
import { useQueryToggle } from '../../../containers/query_toggle';
import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import type { State } from '../../../store';
import { JobIdFilter } from './job_id_filter';
@ -59,13 +59,13 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
[setQuerySkip, setToggleStatus]
);
const { jobIds, loading: loadingJobs } = useInstalledSecurityJobsIds();
const { jobNameById, loading: loadingJobs } = useInstalledSecurityJobNameById();
const jobIds = useMemo(() => Object.keys(jobNameById), [jobNameById]);
const getAnomaliesHostsTableFilterQuerySelector = useMemo(
() => hostsSelectors.hostsAnomaliesJobIdFilterSelector(),
[]
);
const selectedJobIds = useDeepEqualSelector((state: State) =>
getAnomaliesHostsTableFilterQuerySelector(state, type)
);
@ -115,7 +115,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
aggregationInterval: selectedInterval,
});
const hosts = convertAnomaliesToHosts(tableData, hostName);
const hosts = convertAnomaliesToHosts(tableData, jobNameById, hostName);
const columns = getAnomaliesHostTableColumnsCurated(type, startDate, endDate);
const pagination = {
@ -151,6 +151,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
onSelect={onSelectJobId}
selectedJobIds={selectedJobIds}
jobIds={jobIds}
jobNameById={jobNameById}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { mount } from 'enzyme';
import { AnomaliesNetworkTable } from './anomalies_network_table';
import { TestProviders } from '../../../mock';
import React from 'react';
@ -14,28 +13,72 @@ import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { NetworkType } from '../../../../explore/network/store/model';
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
import { FlowTarget } from '../../../../../common/search_strategy';
import { fireEvent, render } from '@testing-library/react';
import { mockAnomalies } from '../mock';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
import { useMlHref } from '@kbn/ml-plugin/public';
jest.mock('../../../containers/query_toggle');
jest.mock('../anomaly/use_anomalies_table_data');
jest.mock('../../../../../common/machine_learning/has_ml_user_permissions');
jest.mock('../hooks/use_installed_security_jobs');
jest.mock('@kbn/ml-plugin/public');
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock;
const mockUseInstalledSecurityJobNameById = useInstalledSecurityJobNameById as jest.Mock;
const mockUseMlHref = useMlHref as jest.Mock;
const mockSetToggle = jest.fn();
(hasMlUserPermissions as jest.Mock).mockReturnValue(true);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseMlHref.mockReturnValue('http://test');
mockUseInstalledSecurityJobNameById.mockReturnValue({
loading: false,
jobNameById: {},
});
const testProps = {
startDate: '2019-07-17T20:00:00.000Z',
endDate: '2019-07-18T20:00:00.000Z',
flowTarget: FlowTarget.destination,
narrowDateRange: jest.fn(),
skip: false,
type: NetworkType.page,
};
describe('Anomalies network table', () => {
describe('toggle query', () => {
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock;
const mockSetToggle = jest.fn();
const testProps = {
startDate: '2019-07-17T20:00:00.000Z',
endDate: '2019-07-18T20:00:00.000Z',
flowTarget: FlowTarget.destination,
narrowDateRange: jest.fn(),
skip: false,
type: NetworkType.page,
it('renders job name when available', () => {
const anomaly = {
...mockAnomalies.anomalies[0],
entityValue: '127.0.0.1',
entityName: 'source.ip',
};
const jobName = 'job_name';
mockUseAnomaliesTableData.mockReturnValue([
false,
{
anomalies: [anomaly],
interval: '10',
},
]);
mockUseInstalledSecurityJobNameById.mockReturnValue({
loading: false,
jobNameById: { [anomaly.jobId]: jobName },
});
const { getByTestId } = render(<AnomaliesNetworkTable {...testProps} />, {
wrapper: TestProviders,
});
expect(getByTestId(`explorer-link-${anomaly.jobId}`).textContent).toContain(jobName);
});
describe('toggle query', () => {
beforeEach(() => {
jest.clearAllMocks();
(hasMlUserPermissions as jest.Mock).mockReturnValue(true);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseAnomaliesTableData.mockReturnValue([
false,
{
@ -46,42 +89,42 @@ describe('Anomalies network table', () => {
});
test('toggleQuery updates toggleStatus', () => {
const wrapper = mount(<AnomaliesNetworkTable {...testProps} />, {
wrappingComponent: TestProviders,
const { getByTestId } = render(<AnomaliesNetworkTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false);
wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click');
fireEvent.click(getByTestId('query-toggle-header'));
expect(mockSetToggle).toBeCalledWith(false);
expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true);
});
test('toggleStatus=true, do not skip', () => {
mount(<AnomaliesNetworkTable {...testProps} />, {
wrappingComponent: TestProviders,
render(<AnomaliesNetworkTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false);
});
test('toggleStatus=true, render components', () => {
const wrapper = mount(<AnomaliesNetworkTable {...testProps} />, {
wrappingComponent: TestProviders,
const { queryByTestId } = render(<AnomaliesNetworkTable {...testProps} />, {
wrapper: TestProviders,
});
expect(wrapper.find('[data-test-subj="network-anomalies-table"]').exists()).toBe(true);
expect(queryByTestId('network-anomalies-table')).toBeInTheDocument();
});
test('toggleStatus=false, do not render components', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
const wrapper = mount(<AnomaliesNetworkTable {...testProps} />, {
wrappingComponent: TestProviders,
const { queryByTestId } = render(<AnomaliesNetworkTable {...testProps} />, {
wrapper: TestProviders,
});
expect(wrapper.find('[data-test-subj="network-anomalies-table"]').exists()).toBe(false);
expect(queryByTestId('network-anomalies-table')).not.toBeInTheDocument();
});
test('toggleStatus=false, skip', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
mount(<AnomaliesNetworkTable {...testProps} />, {
wrappingComponent: TestProviders,
render(<AnomaliesNetworkTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true);

View file

@ -22,7 +22,7 @@ import { BasicTable } from './basic_table';
import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type';
import { Panel } from '../../panel';
import { useQueryToggle } from '../../../containers/query_toggle';
import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import type { State } from '../../../store';
import { JobIdFilter } from './job_id_filter';
@ -60,7 +60,8 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
[setQuerySkip, setToggleStatus]
);
const { jobIds, loading: loadingJobs } = useInstalledSecurityJobsIds();
const { jobNameById, loading: loadingJobs } = useInstalledSecurityJobNameById();
const jobIds = useMemo(() => Object.keys(jobNameById), [jobNameById]);
const getAnomaliesUserTableFilterQuerySelector = useMemo(
() => networkSelectors.networkAnomaliesJobIdFilterSelector(),
@ -113,7 +114,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
aggregationInterval: selectedInterval,
});
const networks = convertAnomaliesToNetwork(tableData, ip);
const networks = convertAnomaliesToNetwork(tableData, jobNameById, ip);
const columns = getAnomaliesNetworkTableColumnsCurated(type, startDate, endDate, flowTarget);
const pagination = {
initialPageIndex: 0,
@ -148,6 +149,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
onSelect={onSelectJobId}
selectedJobIds={selectedJobIds}
jobIds={jobIds}
jobNameById={jobNameById}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -4,8 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mount } from 'enzyme';
import { AnomaliesUserTable } from './anomalies_user_table';
import { TestProviders } from '../../../mock';
import React from 'react';
@ -13,28 +11,80 @@ import { useQueryToggle } from '../../../containers/query_toggle';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { UsersType } from '../../../../explore/users/store/model';
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
import { fireEvent, render } from '@testing-library/react';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
import { mockAnomalies } from '../mock';
import { useMlHref } from '@kbn/ml-plugin/public';
jest.mock('../../../containers/query_toggle');
jest.mock('../anomaly/use_anomalies_table_data');
jest.mock('../../../../../common/machine_learning/has_ml_user_permissions');
jest.mock('../hooks/use_installed_security_jobs');
jest.mock('@kbn/ml-plugin/public');
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock;
const mockUseInstalledSecurityJobNameById = useInstalledSecurityJobNameById as jest.Mock;
const mockUseMlHref = useMlHref as jest.Mock;
const mockSetToggle = jest.fn();
(hasMlUserPermissions as jest.Mock).mockReturnValue(true);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseMlHref.mockReturnValue('http://test');
mockUseInstalledSecurityJobNameById.mockReturnValue({
loading: false,
jobNameById: {},
});
const userName = 'cool_guy';
const testProps = {
startDate: '2019-07-17T20:00:00.000Z',
endDate: '2019-07-18T20:00:00.000Z',
narrowDateRange: jest.fn(),
userName,
skip: false,
type: UsersType.page,
};
describe('Anomalies user table', () => {
describe('toggle query', () => {
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock;
const mockSetToggle = jest.fn();
const testProps = {
startDate: '2019-07-17T20:00:00.000Z',
endDate: '2019-07-18T20:00:00.000Z',
narrowDateRange: jest.fn(),
userName: 'coolguy',
skip: false,
type: UsersType.page,
it('renders job name when available', () => {
const anomaly = {
...mockAnomalies.anomalies[0],
entityValue: userName,
entityName: 'user.name',
};
const jobName = 'job_name';
mockUseAnomaliesTableData.mockReturnValue([
false,
{
anomalies: [anomaly],
interval: '10',
},
]);
mockUseAnomaliesTableData.mockReturnValue([
false,
{
anomalies: [anomaly],
interval: '10',
},
]);
mockUseInstalledSecurityJobNameById.mockReturnValue({
loading: false,
jobNameById: { [anomaly.jobId]: jobName },
});
const { getByTestId } = render(<AnomaliesUserTable {...testProps} />, {
wrapper: TestProviders,
});
expect(getByTestId(`explorer-link-${anomaly.jobId}`).textContent).toContain(jobName);
});
describe('toggle query', () => {
beforeEach(() => {
jest.clearAllMocks();
(hasMlUserPermissions as jest.Mock).mockReturnValue(true);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseAnomaliesTableData.mockReturnValue([
false,
{
@ -45,42 +95,42 @@ describe('Anomalies user table', () => {
});
test('toggleQuery updates toggleStatus', () => {
const wrapper = mount(<AnomaliesUserTable {...testProps} />, {
wrappingComponent: TestProviders,
const { getByTestId } = render(<AnomaliesUserTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false);
wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click');
fireEvent.click(getByTestId('query-toggle-header'));
expect(mockSetToggle).toBeCalledWith(false);
expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true);
});
test('toggleStatus=true, do not skip', () => {
mount(<AnomaliesUserTable {...testProps} />, {
wrappingComponent: TestProviders,
render(<AnomaliesUserTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false);
});
test('toggleStatus=true, render components', () => {
const wrapper = mount(<AnomaliesUserTable {...testProps} />, {
wrappingComponent: TestProviders,
const { queryByTestId } = render(<AnomaliesUserTable {...testProps} />, {
wrapper: TestProviders,
});
expect(wrapper.find('[data-test-subj="user-anomalies-table"]').exists()).toBe(true);
expect(queryByTestId('user-anomalies-table')).toBeInTheDocument();
});
test('toggleStatus=false, do not render components', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
const wrapper = mount(<AnomaliesUserTable {...testProps} />, {
wrappingComponent: TestProviders,
const { queryByTestId } = render(<AnomaliesUserTable {...testProps} />, {
wrapper: TestProviders,
});
expect(wrapper.find('[data-test-subj="user-anomalies-table"]').exists()).toBe(false);
expect(queryByTestId('user-anomalies-table')).not.toBeInTheDocument();
});
test('toggleStatus=false, skip', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle });
mount(<AnomaliesUserTable {...testProps} />, {
wrappingComponent: TestProviders,
render(<AnomaliesUserTable {...testProps} />, {
wrapper: TestProviders,
});
expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true);

View file

@ -30,7 +30,7 @@ import { SelectInterval } from './select_interval';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { usersActions, usersSelectors } from '../../../../explore/users/store';
import type { State } from '../../../store/types';
import { useInstalledSecurityJobsIds } from '../hooks/use_installed_security_jobs';
import { useInstalledSecurityJobNameById } from '../hooks/use_installed_security_jobs';
const sorting = {
sort: {
@ -63,7 +63,8 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
[setQuerySkip, setToggleStatus]
);
const { jobIds, loading: loadingJobs } = useInstalledSecurityJobsIds();
const { jobNameById, loading: loadingJobs } = useInstalledSecurityJobNameById();
const jobIds = useMemo(() => Object.keys(jobNameById), [jobNameById]);
const getAnomaliesUserTableFilterQuerySelector = useMemo(
() => usersSelectors.usersAnomaliesJobIdFilterSelector(),
@ -119,8 +120,7 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
aggregationInterval: selectedInterval,
});
const users = convertAnomaliesToUsers(tableData, userName);
const users = convertAnomaliesToUsers(tableData, jobNameById, userName);
const columns = getAnomaliesUserTableColumnsCurated(type, startDate, endDate);
const pagination = {
initialPageIndex: 0,
@ -156,6 +156,7 @@ const AnomaliesUserTableComponent: React.FC<AnomaliesUserTableProps> = ({
onSelect={onSelectJobId}
selectedJobIds={selectedJobIds}
jobIds={jobIds}
jobNameById={jobNameById}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -18,7 +18,7 @@ describe('convert_anomalies_to_hosts', () => {
});
test('it returns expected anomalies from a host', () => {
const entities = convertAnomaliesToHosts(anomalies);
const entities = convertAnomaliesToHosts(anomalies, {});
const expected: AnomaliesByHost[] = [
{
anomaly: {
@ -61,6 +61,7 @@ describe('convert_anomalies_to_hosts', () => {
time: 1560664800000,
},
hostName: 'zeek-iowa',
jobName: 'job-1',
},
{
anomaly: {
@ -103,13 +104,14 @@ describe('convert_anomalies_to_hosts', () => {
time: 1560664800000,
},
hostName: 'zeek-iowa',
jobName: 'job-2',
},
];
expect(entities).toEqual(expected);
});
test('it returns empty anomalies if sent in a null', () => {
const entities = convertAnomaliesToHosts(null);
const entities = convertAnomaliesToHosts(null, {});
const expected: AnomaliesByHost[] = [];
expect(entities).toEqual(expected);
});
@ -123,7 +125,7 @@ describe('convert_anomalies_to_hosts', () => {
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToHosts(anomalies, 'zeek-iowa');
const entities = convertAnomaliesToHosts(anomalies, {}, 'zeek-iowa');
const expected: AnomaliesByHost[] = [
{
anomaly: {
@ -166,6 +168,7 @@ describe('convert_anomalies_to_hosts', () => {
time: 1560664800000,
},
hostName: 'zeek-iowa',
jobName: 'job-2',
},
];
expect(entities).toEqual(expected);
@ -182,7 +185,7 @@ describe('convert_anomalies_to_hosts', () => {
anomalies.anomalies[1].entityName = 'something-else';
anomalies.anomalies[1].entityValue = 'something-else';
const entities = convertAnomaliesToHosts(anomalies, 'zeek-iowa');
const entities = convertAnomaliesToHosts(anomalies, {}, 'zeek-iowa');
const expected: AnomaliesByHost[] = [
{
anomaly: {
@ -225,13 +228,14 @@ describe('convert_anomalies_to_hosts', () => {
time: 1560664800000,
},
hostName: 'zeek-iowa',
jobName: 'job-2',
},
];
expect(entities).toEqual(expected);
});
test('it returns empty anomalies if sent in the name of one that does not exist', () => {
const entities = convertAnomaliesToHosts(anomalies, 'some-made-up-name-here-for-you');
const entities = convertAnomaliesToHosts(anomalies, {}, 'some-made-up-name-here-for-you');
const expected: AnomaliesByHost[] = [];
expect(entities).toEqual(expected);
});

View file

@ -10,6 +10,7 @@ import { getHostNameFromInfluencers } from '../influencers/get_host_name_from_in
export const convertAnomaliesToHosts = (
anomalies: Anomalies | null,
jobNameById: Record<string, string | undefined>,
hostName?: string
): AnomaliesByHost[] => {
if (anomalies == null) {
@ -17,11 +18,25 @@ export const convertAnomaliesToHosts = (
} else {
return anomalies.anomalies.reduce<AnomaliesByHost[]>((accum, item) => {
if (getHostNameFromEntity(item, hostName)) {
return [...accum, { hostName: item.entityValue, anomaly: item }];
return [
...accum,
{
hostName: item.entityValue,
jobName: jobNameById[item.jobId] ?? item.jobId,
anomaly: item,
},
];
} else {
const hostNameFromInfluencers = getHostNameFromInfluencers(item.influencers, hostName);
if (hostNameFromInfluencers != null) {
return [...accum, { hostName: hostNameFromInfluencers, anomaly: item }];
return [
...accum,
{
hostName: hostNameFromInfluencers,
jobName: jobNameById[item.jobId] ?? item.jobId,
anomaly: item,
},
];
} else {
return accum;
}

View file

@ -20,7 +20,7 @@ describe('convert_anomalies_to_hosts', () => {
test('it returns expected anomalies from a network if is part of the entityName and is a source.ip', () => {
anomalies.anomalies[0].entityName = 'source.ip';
anomalies.anomalies[0].entityValue = '127.0.0.1';
const entities = convertAnomaliesToNetwork(anomalies);
const entities = convertAnomaliesToNetwork(anomalies, {});
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -64,6 +64,7 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'source.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
@ -72,7 +73,7 @@ describe('convert_anomalies_to_hosts', () => {
test('it returns expected anomalies from a network if is part of the entityName and is a destination.ip', () => {
anomalies.anomalies[0].entityName = 'destination.ip';
anomalies.anomalies[0].entityValue = '127.0.0.1';
const entities = convertAnomaliesToNetwork(anomalies);
const entities = convertAnomaliesToNetwork(anomalies, {});
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -116,6 +117,7 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'destination.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
@ -125,7 +127,7 @@ describe('convert_anomalies_to_hosts', () => {
anomalies.anomalies[0].entityName = 'not-an-ip';
anomalies.anomalies[0].entityValue = 'not-an-ip';
anomalies.anomalies[0].influencers = [{ 'source.ip': '127.0.0.1' }];
const entities = convertAnomaliesToNetwork(anomalies);
const entities = convertAnomaliesToNetwork(anomalies, {});
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -165,6 +167,7 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'source.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
@ -174,7 +177,7 @@ describe('convert_anomalies_to_hosts', () => {
anomalies.anomalies[0].entityName = 'not-an-ip';
anomalies.anomalies[0].entityValue = 'not-an-ip';
anomalies.anomalies[0].influencers = [{ 'destination.ip': '127.0.0.1' }];
const entities = convertAnomaliesToNetwork(anomalies);
const entities = convertAnomaliesToNetwork(anomalies, {});
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -214,13 +217,14 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'destination.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
});
test('it returns empty anomalies if sent in a null', () => {
const entities = convertAnomaliesToNetwork(null);
const entities = convertAnomaliesToNetwork(null, {});
const expected: AnomaliesByNetwork[] = [];
expect(entities).toEqual(expected);
});
@ -233,7 +237,7 @@ describe('convert_anomalies_to_hosts', () => {
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const entities = convertAnomaliesToNetwork(anomalies, {}, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -277,6 +281,7 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'source.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
@ -290,7 +295,7 @@ describe('convert_anomalies_to_hosts', () => {
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const entities = convertAnomaliesToNetwork(anomalies, {}, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -334,6 +339,7 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'destination.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
@ -347,7 +353,7 @@ describe('convert_anomalies_to_hosts', () => {
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const entities = convertAnomaliesToNetwork(anomalies, {}, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -391,6 +397,7 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'source.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
@ -404,7 +411,7 @@ describe('convert_anomalies_to_hosts', () => {
{ 'process.name': 'du' },
{ 'user.name': 'root' },
];
const entities = convertAnomaliesToNetwork(anomalies, '127.0.0.1');
const entities = convertAnomaliesToNetwork(anomalies, {}, '127.0.0.1');
const expected: AnomaliesByNetwork[] = [
{
anomaly: {
@ -448,13 +455,14 @@ describe('convert_anomalies_to_hosts', () => {
},
ip: '127.0.0.1',
type: 'destination.ip',
jobName: 'job-1',
},
];
expect(entities).toEqual(expected);
});
test('it returns empty anomalies if sent in the name of one that does not exist', () => {
const entities = convertAnomaliesToNetwork(anomalies, 'some-made-up-name-here-for-you');
const entities = convertAnomaliesToNetwork(anomalies, {}, 'some-made-up-name-here-for-you');
const expected: AnomaliesByNetwork[] = [];
expect(entities).toEqual(expected);
});

View file

@ -11,6 +11,7 @@ import { getNetworkFromInfluencers } from '../influencers/get_network_from_influ
export const convertAnomaliesToNetwork = (
anomalies: Anomalies | null,
jobNameById: Record<string, string | undefined>,
ip?: string
): AnomaliesByNetwork[] => {
if (anomalies == null) {
@ -18,11 +19,27 @@ export const convertAnomaliesToNetwork = (
} else {
return anomalies.anomalies.reduce<AnomaliesByNetwork[]>((accum, item) => {
if (isDestinationOrSource(item.entityName) && getNetworkFromEntity(item, ip)) {
return [...accum, { ip: item.entityValue, type: item.entityName, anomaly: item }];
return [
...accum,
{
ip: item.entityValue,
type: item.entityName,
jobName: jobNameById[item.jobId] ?? item.jobId,
anomaly: item,
},
];
} else {
const network = getNetworkFromInfluencers(item.influencers, ip);
if (network != null) {
return [...accum, { ip: network.ip, type: network.type, anomaly: item }];
return [
...accum,
{
ip: network.ip,
type: network.type,
jobName: jobNameById[item.jobId] ?? item.jobId,
anomaly: item,
},
];
} else {
return accum;
}

View file

@ -11,23 +11,25 @@ import type { AnomaliesByUser } from '../types';
describe('convert_anomalies_to_users', () => {
test('it returns expected anomalies from a user', () => {
const entities = convertAnomaliesToUsers(mockAnomalies);
const entities = convertAnomaliesToUsers(mockAnomalies, {});
const expected: AnomaliesByUser[] = [
{
anomaly: mockAnomalies.anomalies[0],
userName: 'root',
jobName: 'job-1',
},
{
anomaly: mockAnomalies.anomalies[1],
userName: 'root',
jobName: 'job-2',
},
];
expect(entities).toEqual(expected);
});
test('it returns empty anomalies if sent in a null', () => {
const entities = convertAnomaliesToUsers(null);
const entities = convertAnomaliesToUsers(null, {});
const expected: AnomaliesByUser[] = [];
expect(entities).toEqual(expected);
});
@ -50,11 +52,12 @@ describe('convert_anomalies_to_users', () => {
],
};
const entities = convertAnomaliesToUsers(anomalies, 'root');
const entities = convertAnomaliesToUsers(anomalies, {}, 'root');
const expected: AnomaliesByUser[] = [
{
anomaly: anomalies.anomalies[1],
userName: 'root',
jobName: 'job-2',
},
];
expect(entities).toEqual(expected);
@ -82,18 +85,19 @@ describe('convert_anomalies_to_users', () => {
],
};
const entities = convertAnomaliesToUsers(anomalies, 'root');
const entities = convertAnomaliesToUsers(anomalies, {}, 'root');
const expected: AnomaliesByUser[] = [
{
anomaly: anomalies.anomalies[1],
userName: 'root',
jobName: 'job-2',
},
];
expect(entities).toEqual(expected);
});
test('it returns empty anomalies if sent in the name of one that does not exist', () => {
const entities = convertAnomaliesToUsers(mockAnomalies, 'some-made-up-name-here-for-you');
const entities = convertAnomaliesToUsers(mockAnomalies, {}, 'some-made-up-name-here-for-you');
const expected: AnomaliesByUser[] = [];
expect(entities).toEqual(expected);
});

View file

@ -10,6 +10,7 @@ import { getUserNameFromInfluencers } from '../influencers/get_user_name_from_in
export const convertAnomaliesToUsers = (
anomalies: Anomalies | null,
jobNameById: Record<string, string | undefined>,
userName?: string
): AnomaliesByUser[] => {
if (anomalies == null) {
@ -17,11 +18,25 @@ export const convertAnomaliesToUsers = (
} else {
return anomalies.anomalies.reduce<AnomaliesByUser[]>((accum, item) => {
if (getUserNameFromEntity(item, userName)) {
return [...accum, { userName: item.entityValue, anomaly: item }];
return [
...accum,
{
userName: item.entityValue,
jobName: jobNameById[item.jobId] ?? item.jobId,
anomaly: item,
},
];
} else {
const userNameFromInfluencers = getUserNameFromInfluencers(item.influencers, userName);
if (userNameFromInfluencers != null) {
return [...accum, { userName: userNameFromInfluencers, anomaly: item }];
return [
...accum,
{
userName: userNameFromInfluencers,
jobName: jobNameById[item.jobId] ?? item.jobId,
anomaly: item,
},
];
} else {
return accum;
}

View file

@ -32,6 +32,7 @@ describe('getAnomaliesDefaultTableColumns', () => {
AnomaliesBy
>;
const anomaly: AnomaliesBy = {
jobName: undefined,
anomaly: {
detectorIndex: 0,
entityName: 'entity-name-1',

View file

@ -32,14 +32,14 @@ export const getAnomaliesDefaultTableColumns = (
] => [
{
name: i18n.DETECTOR,
field: 'anomaly.jobId',
field: 'jobName',
sortable: true,
render: (jobId, anomalyBy) => (
render: (jobName, anomalyBy) => (
<ExplorerLink
score={anomalyBy.anomaly}
startDate={startDate}
endDate={endDate}
linkName={jobId}
linkName={jobName}
/>
),
},

View file

@ -20,13 +20,20 @@ const withTheme = (storyFn: () => ReactNode) => (
storiesOf('JobIdFilter', module)
.addDecorator(withTheme)
.add('empty', () => (
<JobIdFilter title="Job id" selectedJobIds={[]} jobIds={[]} onSelect={action('onSelect')} />
<JobIdFilter
title="Job id"
selectedJobIds={[]}
jobIds={[]}
jobNameById={{}}
onSelect={action('onSelect')}
/>
))
.add('one selected item', () => (
<JobIdFilter
title="Job id"
selectedJobIds={['test_job_1']}
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
jobNameById={{}}
onSelect={action('onSelect')}
/>
))
@ -35,6 +42,7 @@ storiesOf('JobIdFilter', module)
title="Job id"
selectedJobIds={['test_job_2', 'test_job_3']}
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
jobNameById={{}}
onSelect={action('onSelect')}
/>
))
@ -43,6 +51,7 @@ storiesOf('JobIdFilter', module)
title="Job id"
selectedJobIds={[]}
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
jobNameById={{}}
onSelect={action('onSelect')}
/>
));

View file

@ -9,10 +9,18 @@ import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import { JobIdFilter } from './job_id_filter';
const JOB_IDS = ['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4'];
describe('JobIdFilter', () => {
it('is disabled when job id is empty', () => {
const { getByTestId } = render(
<JobIdFilter title="Job id" selectedJobIds={[]} jobIds={[]} onSelect={jest.fn()} />
<JobIdFilter
title="Job id"
selectedJobIds={[]}
jobIds={[]}
onSelect={jest.fn()}
jobNameById={{}}
/>
);
expect(getByTestId('job-id-filter-button')).toBeDisabled();
});
@ -23,8 +31,9 @@ describe('JobIdFilter', () => {
<JobIdFilter
title="Job id"
selectedJobIds={[]}
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
jobIds={JOB_IDS}
onSelect={onSelectCb}
jobNameById={{}}
/>
);
fireEvent.click(getByTestId('job-id-filter-button'));
@ -38,8 +47,9 @@ describe('JobIdFilter', () => {
<JobIdFilter
title="Job id"
selectedJobIds={['test_job_2']}
jobIds={['test_job_1', 'test_job_2', 'test_job_3', 'test_job_4']}
jobIds={JOB_IDS}
onSelect={jest.fn()}
jobNameById={{}}
/>
);
@ -49,4 +59,21 @@ describe('JobIdFilter', () => {
getByTestId('job-id-filter-item-test_job_2').querySelector('span[data-euiicon-type=check]')
).toBeInTheDocument();
});
it('displays job name when it is available', () => {
const jobName = 'TEST_JOB_NAME';
const { getByTestId } = render(
<JobIdFilter
title="Job id"
selectedJobIds={[]}
jobIds={['test_job']}
onSelect={jest.fn()}
jobNameById={{ test_job: jobName }}
/>
);
fireEvent.click(getByTestId('job-id-filter-button'));
expect(getByTestId('job-id-filter-item-test_job').textContent).toBe(jobName);
});
});

View file

@ -10,9 +10,10 @@ import { EuiFilterButton, EuiFilterGroup, EuiFilterSelectItem, EuiPopover } from
export const JobIdFilter: React.FC<{
selectedJobIds: string[];
jobIds: string[];
jobNameById: Record<string, string | undefined>;
onSelect: (jobIds: string[]) => void;
title: string;
}> = ({ selectedJobIds, onSelect, title, jobIds }) => {
}> = ({ selectedJobIds, onSelect, title, jobIds, jobNameById }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = useCallback(() => {
@ -50,7 +51,7 @@ export const JobIdFilter: React.FC<{
{title}
</EuiFilterButton>
),
[isPopoverOpen, onButtonClick, title, selectedJobIds.length, jobIds]
[jobIds.length, selectedJobIds.length, isPopoverOpen, onButtonClick, title]
);
return (
@ -69,7 +70,7 @@ export const JobIdFilter: React.FC<{
key={id}
onClick={() => updateSelection(id)}
>
{id}
{jobNameById[id] ?? id}
</EuiFilterSelectItem>
))}
</div>

View file

@ -65,6 +65,7 @@ export type NarrowDateRange = (score: Anomaly, interval: string) => void;
export interface AnomaliesBy {
anomaly: Anomaly;
jobName: string | undefined;
}
export interface AnomaliesByHost extends AnomaliesBy {

View file

@ -45,12 +45,18 @@ export const filterJobs = ({
* @param jobs to filter
* @param filterQuery user-provided search string to filter for occurrence in job names/description
*/
export const searchFilter = (jobs: SecurityJob[], filterQuery?: string): SecurityJob[] =>
jobs.filter((job) =>
filterQuery == null
export const searchFilter = (jobs: SecurityJob[], filterQuery?: string): SecurityJob[] => {
const lowerCaseFilterQuery = filterQuery?.toLowerCase();
return jobs.filter((job) =>
lowerCaseFilterQuery == null
? true
: job.id.includes(filterQuery) || job.description.includes(filterQuery)
: job.id.includes(lowerCaseFilterQuery) ||
job.customSettings?.security_app_display_name
?.toLowerCase()
?.includes(lowerCaseFilterQuery) ||
job.description.toLowerCase().includes(lowerCaseFilterQuery)
);
};
/**
* Given an array of titles this will always return the same string for usage within

View file

@ -49,6 +49,31 @@ describe('useSecurityJobsHelpers', () => {
memory_status: '',
moduleId: 'security_linux_v3',
processed_record_count: 0,
customSettings: {
created_by: 'ml-module-siem-auditbeat',
custom_urls: [
{
url_name: 'Host Details by process name',
url_value:
"siem#/ml-hosts/$host.name$?kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
},
{
url_name: 'Host Details by user name',
url_value:
"siem#/ml-hosts/$host.name$?kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
},
{
url_name: 'Hosts Overview by process name',
url_value:
"siem#/ml-hosts?kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
},
{
url_name: 'Hosts Overview by user name',
url_value:
"siem#/ml-hosts?kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))",
},
],
},
});
});

View file

@ -46,6 +46,7 @@ export const moduleToSecurityJob = (
awaitingNodeAssignment: false,
jobTags: {},
bucketSpanSeconds: 900,
customSettings: moduleJob.config.custom_settings,
};
};

View file

@ -75,8 +75,12 @@ const getJobsTableColumns = (
) => [
{
name: i18n.COLUMN_JOB_NAME,
render: ({ id, description }: SecurityJob) => (
<JobName id={id} description={description} basePath={basePath} />
render: ({ id, description, customSettings }: SecurityJob) => (
<JobName
id={customSettings?.security_app_display_name ?? id}
description={description}
basePath={basePath}
/>
),
},
{

View file

@ -74,6 +74,7 @@ export interface ModuleJob {
custom_settings: {
created_by: string;
custom_urls: CustomURL[];
security_app_display_name?: string;
};
job_type: string;
};
@ -121,6 +122,9 @@ export interface SecurityJob extends MlSummaryJob {
isCompatible: boolean;
isInstalled: boolean;
isElasticJob: boolean;
customSettings?: {
security_app_display_name?: string;
};
}
export interface AugmentedSecurityJobFields {

View file

@ -204,7 +204,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
{({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => (
<HostOverviewManage
id={id}
isInDetailsSidePanel={false}
@ -220,6 +220,7 @@ const HostDetailsComponent: React.FC<HostDetailsProps> = ({ detailName, hostDeta
inspect={inspect}
hostName={detailName}
indexNames={selectedPatterns}
jobNameById={jobNameById}
/>
)}
</AnomalyTableProvider>

View file

@ -144,6 +144,7 @@ exports[`IP Overview Component rendering it renders the default IP Overview 1`]
ip="10.10.10.10"
isInDetailsSidePanel={false}
isLoadingAnomaliesData={false}
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"
@ -296,6 +297,7 @@ exports[`IP Overview Component rendering it renders the side panel IP overview 1
ip="10.10.10.10"
isInDetailsSidePanel={true}
isLoadingAnomaliesData={false}
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"

View file

@ -55,6 +55,7 @@ describe('IP Overview Component', () => {
flowTarget: FlowTargetSourceDest;
}>,
indexPatterns: [],
jobNameById: {},
};
test('it renders the default IP Overview', () => {

View file

@ -56,6 +56,7 @@ export interface IpOverviewProps {
startDate: string;
type: networkModel.NetworkType;
indexPatterns: string[];
jobNameById: Record<string, string | undefined>;
}
export const IpOverview = React.memo<IpOverviewProps>(
@ -74,6 +75,7 @@ export const IpOverview = React.memo<IpOverviewProps>(
anomaliesData,
narrowDateRange,
indexPatterns,
jobNameById,
}) => {
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
@ -109,6 +111,7 @@ export const IpOverview = React.memo<IpOverviewProps>(
endDate={endDate}
isLoading={isLoadingAnomaliesData}
narrowDateRange={narrowDateRange}
jobNameById={jobNameById}
/>
),
},

View file

@ -51,7 +51,7 @@ import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml
import { useAlertsPrivileges } from '../../../../detections/containers/detection_engine/alerts/use_alerts_privileges';
import { navTabsNetworkDetails } from './nav_tabs';
import { NetworkDetailsTabs } from './details_tabs';
import { useInstalledSecurityJobsIds } from '../../../../common/components/ml/hooks/use_installed_security_jobs';
import { useInstalledSecurityJobNameById } from '../../../../common/components/ml/hooks/use_installed_security_jobs';
export { getTrailingBreadcrumbs } from './utils';
@ -137,7 +137,8 @@ const NetworkDetailsComponent: React.FC = () => {
ip,
});
const { jobIds } = useInstalledSecurityJobsIds();
const { jobNameById } = useInstalledSecurityJobNameById();
const jobIds = useMemo(() => Object.keys(jobNameById), [jobNameById]);
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
criteriaFields: networkToCriteria(detailName, flowTarget),
startDate: from,
@ -202,6 +203,7 @@ const NetworkDetailsComponent: React.FC = () => {
endDate={to}
narrowDateRange={narrowDateRange}
indexPatterns={selectedPatterns}
jobNameById={jobNameById}
/>
<EuiHorizontalRule />

View file

@ -197,7 +197,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
{({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => (
<UserOverview
userName={detailName}
id={QUERY_ID}
@ -210,6 +210,7 @@ const UsersDetailsComponent: React.FC<UsersDetailsProps> = ({
endDate={to}
narrowDateRange={narrowDateRange}
indexPatterns={selectedPatterns}
jobNameById={jobNameById}
/>
)}
</AnomalyTableProvider>

View file

@ -45,11 +45,11 @@ export const useAnomaliesColumns = (
truncateText: true,
mobileOptions: { show: true },
'data-test-subj': 'anomalies-table-column-name',
render: (name, { count, job }) => {
render: (jobName, { count, job }) => {
if (count > 0 || (job && isJobStarted(job.jobState, job.datafeedState))) {
return name;
return jobName;
} else {
return <MediumShadeText>{name}</MediumShadeText>;
return <MediumShadeText>{jobName}</MediumShadeText>;
}
},
},

View file

@ -198,6 +198,7 @@ exports[`Host Summary Component it renders the default Host Summary 1`] = `
indexNames={Array []}
isInDetailsSidePanel={false}
isLoadingAnomaliesData={false}
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"
@ -402,6 +403,7 @@ exports[`Host Summary Component it renders the panel view Host Summary 1`] = `
indexNames={Array []}
isInDetailsSidePanel={true}
isLoadingAnomaliesData={false}
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"

View file

@ -42,6 +42,7 @@ describe('Host Summary Component', () => {
narrowDateRange: jest.fn(),
startDate: '2019-06-15T06:00:00.000Z',
hostName: 'testHostName',
jobNameById: {},
};
beforeEach(() => {

View file

@ -54,6 +54,7 @@ interface HostSummaryProps {
endDate: string;
narrowDateRange: NarrowDateRange;
hostName: string;
jobNameById: Record<string, string | undefined>;
}
const HostRiskOverviewWrapper = styled(EuiFlexGroup)`
@ -76,6 +77,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
narrowDateRange,
startDate,
hostName,
jobNameById,
}) => {
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
@ -198,6 +200,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
endDate={endDate}
isLoading={isLoadingAnomaliesData}
narrowDateRange={narrowDateRange}
jobNameById={jobNameById}
/>
),
},
@ -211,6 +214,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
narrowDateRange,
startDate,
userPermissions,
jobNameById,
]
);

View file

@ -171,6 +171,7 @@ exports[`User Summary Component it renders the default User Summary 1`] = `
indexPatterns={Array []}
isInDetailsSidePanel={false}
isLoadingAnomaliesData={false}
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"
@ -349,6 +350,7 @@ exports[`User Summary Component it renders the panel view User Summary 1`] = `
indexPatterns={Array []}
isInDetailsSidePanel={true}
isLoadingAnomaliesData={false}
jobNameById={Object {}}
loading={false}
narrowDateRange={[MockFunction]}
startDate="2019-06-15T06:00:00.000Z"

View file

@ -55,6 +55,7 @@ describe('User Summary Component', () => {
startDate: '2019-06-15T06:00:00.000Z',
userName: 'testUserName',
indexPatterns: [],
jobNameById: {},
};
beforeEach(() => {

View file

@ -52,6 +52,7 @@ export interface UserSummaryProps {
narrowDateRange: NarrowDateRange;
userName: string;
indexPatterns: string[];
jobNameById: Record<string, string | undefined>;
}
const UserRiskOverviewWrapper = styled(EuiFlexGroup)`
@ -74,6 +75,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
endDate,
userName,
indexPatterns,
jobNameById,
}) => {
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
@ -179,6 +181,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
endDate={endDate}
isLoading={isLoadingAnomaliesData}
narrowDateRange={narrowDateRange}
jobNameById={jobNameById}
/>
),
},
@ -192,6 +195,7 @@ export const UserOverview = React.memo<UserSummaryProps>(
narrowDateRange,
startDate,
userPermissions,
jobNameById,
]
);

View file

@ -33,8 +33,9 @@ jest.mock('../../../../common/components/ml/anomaly/anomaly_table_provider', ()
children: (args: {
anomaliesData: Anomalies;
isLoadingAnomaliesData: boolean;
jobNameById: Record<string, string | undefined>;
}) => React.ReactNode;
}) => children({ anomaliesData: mockAnomalies, isLoadingAnomaliesData: false }),
}) => children({ anomaliesData: mockAnomalies, isLoadingAnomaliesData: false, jobNameById: {} }),
}));
describe('Expandable Host Component', () => {

View file

@ -95,7 +95,7 @@ export const ExpandableHostDetails = ({
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
{({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => (
<HostOverview
contextID={contextID}
id={ID}
@ -110,6 +110,7 @@ export const ExpandableHostDetails = ({
endDate={to}
narrowDateRange={narrowDateRange}
hostName={hostName}
jobNameById={jobNameById}
/>
)}
</AnomalyTableProvider>

View file

@ -29,7 +29,7 @@ import { useNetworkDetails } from '../../../../explore/network/containers/detail
import { networkModel } from '../../../../explore/network/store';
import { useAnomaliesTableData } from '../../../../common/components/ml/anomaly/use_anomalies_table_data';
import { LandingCards } from '../../../../common/components/landing_cards';
import { useInstalledSecurityJobsIds } from '../../../../common/components/ml/hooks/use_installed_security_jobs';
import { useInstalledSecurityJobNameById } from '../../../../common/components/ml/hooks/use_installed_security_jobs';
interface ExpandableNetworkProps {
expandedNetwork: { ip: string; flowTarget: FlowTargetSourceDest };
@ -116,7 +116,8 @@ export const ExpandableNetworkDetails = ({
});
useInvalidFilterQuery({ id, filterQuery, kqlError, query, startDate: from, endDate: to });
const { jobIds } = useInstalledSecurityJobsIds();
const { jobNameById } = useInstalledSecurityJobNameById();
const jobIds = useMemo(() => Object.keys(jobNameById), [jobNameById]);
const [isLoadingAnomaliesData, anomaliesData] = useAnomaliesTableData({
criteriaFields: networkToCriteria(ip, flowTarget),
startDate: from,
@ -143,6 +144,7 @@ export const ExpandableNetworkDetails = ({
endDate={to}
narrowDateRange={narrowDateRange}
indexPatterns={selectedPatterns}
jobNameById={jobNameById}
/>
) : (
<LandingCards />

View file

@ -33,8 +33,9 @@ jest.mock('../../../../common/components/ml/anomaly/anomaly_table_provider', ()
children: (args: {
anomaliesData: Anomalies;
isLoadingAnomaliesData: boolean;
jobNameById: Record<string, string | undefined>;
}) => React.ReactNode;
}) => children({ anomaliesData: mockAnomalies, isLoadingAnomaliesData: false }),
}) => children({ anomaliesData: mockAnomalies, isLoadingAnomaliesData: false, jobNameById: {} }),
}));
describe('Expandable Host Component', () => {

View file

@ -90,7 +90,7 @@ export const ExpandableUserDetails = ({
endDate={to}
skip={isInitializing}
>
{({ isLoadingAnomaliesData, anomaliesData }) => (
{({ isLoadingAnomaliesData, anomaliesData, jobNameById }) => (
<UserOverview
userName={userName}
isInDetailsSidePanel={true}
@ -105,6 +105,7 @@ export const ExpandableUserDetails = ({
endDate={to}
narrowDateRange={narrowDateRange}
indexPatterns={selectedPatterns}
jobNameById={jobNameById}
/>
)}
</AnomalyTableProvider>