[Security Solution] Fix: destination of the command link is a host details page (#188742)

## Summary
It doesn't look right that the destination of the command link is a host
details page.
In this PR the command link has been removed and was replaced with a
normal text.
The issue related with this matter is below:
https://github.com/elastic/kibana/issues/188295

- Before:


https://github.com/user-attachments/assets/78d4a09e-e531-4722-b6af-fe7068b29ad5

- Now:

<img width="399" alt="Screenshot 2024-07-19 at 14 16 05"
src="https://github.com/user-attachments/assets/8f8b51e1-3aa6-4d00-8ebc-a98db4afaef0">

### Checklist

Delete any items that are not applicable to this PR.

- [ ] [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: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Agustina Nahir Ruidiaz 2024-07-23 13:31:10 +02:00 committed by GitHub
parent c11d5345b8
commit abfd30da75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 205 additions and 172 deletions

View file

@ -40,7 +40,7 @@ import type {
NetworkTopCountriesColumnsNetworkDetails,
} from '../../network/components/network_top_countries_table/columns';
import type { TlsColumns } from '../../network/components/tls_table/columns';
import type { UncommonProcessTableColumns } from '../../hosts/components/uncommon_process_table';
import type { UncommonProcessTableColumns } from '../../hosts/components/uncommon_process_table/columns';
import type { HostRiskScoreColumns } from '../../../entity_analytics/components/host_risk_score_table';
import type { UsersColumns } from '../../network/components/users_table/columns';

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getOr } from 'lodash/fp';
import React from 'react';
import { hostsModel } from '../../store';
import { mockData } from './mock';
import { HostsType } from '../../store/model';
import * as i18n from './translations';
import { getUncommonColumnsCurated, getHostNames } from './columns';
jest.mock('../../../../common/lib/kibana');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
EuiScreenReaderOnly: () => <></>,
};
});
jest.mock('../../../../common/components/link_to');
describe('Uncommon Process Columns', () => {
const loadPage = jest.fn();
const defaultProps = {
data: mockData.edges,
fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo),
id: 'uncommonProcess',
isInspect: false,
loading: false,
loadPage,
setQuerySkip: jest.fn(),
showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo),
totalCount: mockData.totalCount,
type: hostsModel.HostsType.page,
};
describe('#getHostNames', () => {
test('when hosts is an empty array, it should return an empty array', () => {
const hostNames = getHostNames(defaultProps.data[0].node.hosts);
expect(hostNames.length).toEqual(0);
});
test('when hosts is an array with one elem, it should return an array with the name property of the item', () => {
const hostNames = getHostNames(defaultProps.data[1].node.hosts);
expect(hostNames.length).toEqual(1);
});
test('when hosts is an array with two elem, it should return an array with each name of each item', () => {
const hostNames = getHostNames(defaultProps.data[2].node.hosts);
expect(hostNames.length).toEqual(2);
});
test('when hosts is an array with items without name prop, it should return an empty array', () => {
const hostNames = getHostNames(defaultProps.data[3].node.hosts);
expect(hostNames.length).toEqual(0);
});
});
describe('#getUncommonColumnsCurated', () => {
test('on hosts page, we expect to get all columns', () => {
expect(getUncommonColumnsCurated(HostsType.page).length).toEqual(6);
});
test('on host details page, we expect to remove two columns', () => {
const columns = getUncommonColumnsCurated(HostsType.details);
expect(columns.length).toEqual(4);
});
test('on host page, we should have hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.page);
expect(columns.some((col) => col.name === i18n.HOSTS)).toEqual(true);
});
test('on host page, we should have number of hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.page);
expect(columns.some((col) => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(true);
});
test('on host details page, we should not have hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.details);
expect(columns.some((col) => col.name === i18n.HOSTS)).toEqual(false);
});
test('on host details page, we should not have number of hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.details);
expect(columns.some((col) => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(false);
});
});
});

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { HostEcs } from '@kbn/securitysolution-ecs';
import type { Columns } from '../../../components/paginated_table';
import { HostDetailsLink } from '../../../../common/components/links';
import { defaultToEmptyTag, getEmptyValue } from '../../../../common/components/empty_value';
import { getRowItemsWithActions } from '../../../../common/components/tables/helpers';
import type { HostsUncommonProcessesEdges } from '../../../../../common/search_strategy';
import { HostsType } from '../../store/model';
import * as i18n from './translations';
export type UncommonProcessTableColumns = Array<Columns<HostsUncommonProcessesEdges>>;
export const getHostNames = (hosts: HostEcs[]): string[] => {
if (!hosts) return [];
return hosts
.filter((host) => host.name != null && host.name[0] != null)
.map((host) => (host.name != null && host.name[0] != null ? host.name[0] : ''));
};
export const getUncommonColumns = (): UncommonProcessTableColumns => [
{
name: i18n.NAME,
truncateText: false,
mobileOptions: { show: true },
width: '20%',
render: ({ node }) =>
getRowItemsWithActions({
values: node.process.name,
fieldName: 'process.name',
idPrefix: `uncommon-process-table-${node._id}-processName`,
}),
},
{
align: 'right',
name: i18n.NUMBER_OF_HOSTS,
truncateText: false,
mobileOptions: { show: true },
render: ({ node }) => <>{node.hosts != null ? node.hosts.length : getEmptyValue()}</>,
width: '8%',
},
{
align: 'right',
name: i18n.NUMBER_OF_INSTANCES,
truncateText: false,
mobileOptions: { show: true },
render: ({ node }) => defaultToEmptyTag(node.instances),
width: '8%',
},
{
name: i18n.HOSTS,
truncateText: false,
mobileOptions: { show: true },
width: '25%',
render: ({ node }) =>
getRowItemsWithActions({
values: getHostNames(node.hosts),
fieldName: 'host.name',
idPrefix: `uncommon-process-table-${node._id}-processHost`,
render: (item) => <HostDetailsLink hostName={item} />,
}),
},
{
name: i18n.LAST_COMMAND,
truncateText: false,
mobileOptions: { show: true },
width: '25%',
render: ({ node }) =>
getRowItemsWithActions({
values: node.process != null ? node.process.args : null,
fieldName: 'process.args',
idPrefix: `uncommon-process-table-${node._id}-processArgs`,
displayCount: 1,
}),
},
{
name: i18n.LAST_USER,
truncateText: false,
mobileOptions: { show: true },
render: ({ node }) =>
getRowItemsWithActions({
values: node.user != null ? node.user.name : null,
fieldName: 'user.name',
idPrefix: `uncommon-process-table-${node._id}-processUser`,
}),
},
];
export const getUncommonColumnsCurated = (pageType: HostsType): UncommonProcessTableColumns => {
const columns: UncommonProcessTableColumns = getUncommonColumns();
if (pageType === HostsType.details) {
const columnsToRemove = new Set([i18n.HOSTS, i18n.NUMBER_OF_HOSTS]);
return columns.filter(
(column) => typeof column.name === 'string' && !columnsToRemove.has(column.name)
);
}
return columns;
};

View file

@ -14,10 +14,8 @@ import { hostsModel } from '../../store';
import { getEmptyValue } from '../../../../common/components/empty_value';
import { useMountAppended } from '../../../../common/utils/use_mount_appended';
import { getArgs, UncommonProcessTable, getUncommonColumnsCurated } from '.';
import { UncommonProcessTable } from '.';
import { mockData } from './mock';
import { HostsType } from '../../store/model';
import * as i18n from './translations';
jest.mock('../../../../common/lib/kibana');
@ -151,55 +149,4 @@ describe('Uncommon Process Table Component', () => {
);
});
});
describe('#getArgs', () => {
test('it works with string array', () => {
const args = ['1', '2', '3'];
expect(getArgs(args)).toEqual('1 2 3');
});
test('it returns null if empty array', () => {
const args: string[] = [];
expect(getArgs(args)).toEqual(null);
});
test('it returns null if given null', () => {
expect(getArgs(null)).toEqual(null);
});
test('it returns null if given undefined', () => {
expect(getArgs(undefined)).toEqual(null);
});
});
describe('#getUncommonColumnsCurated', () => {
test('on hosts page, we expect to get all columns', () => {
expect(getUncommonColumnsCurated(HostsType.page).length).toEqual(6);
});
test('on host details page, we expect to remove two columns', () => {
const columns = getUncommonColumnsCurated(HostsType.details);
expect(columns.length).toEqual(4);
});
test('on host page, we should have hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.page);
expect(columns.some((col) => col.name === i18n.HOSTS)).toEqual(true);
});
test('on host page, we should have number of hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.page);
expect(columns.some((col) => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(true);
});
test('on host details page, we should not have hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.details);
expect(columns.some((col) => col.name === i18n.HOSTS)).toEqual(false);
});
test('on host details page, we should not have number of hosts', () => {
const columns = getUncommonColumnsCurated(HostsType.details);
expect(columns.some((col) => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(false);
});
});
});

View file

@ -8,17 +8,13 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import type { HostEcs } from '@kbn/securitysolution-ecs';
import type { HostsUncommonProcessesEdges } from '../../../../../common/search_strategy';
import { hostsActions, hostsModel, hostsSelectors } from '../../store';
import { defaultToEmptyTag, getEmptyValue } from '../../../../common/components/empty_value';
import { HostDetailsLink } from '../../../../common/components/links';
import type { Columns, ItemsPerRow } from '../../../components/paginated_table';
import type { ItemsPerRow } from '../../../components/paginated_table';
import { PaginatedTable } from '../../../components/paginated_table';
import * as i18n from './translations';
import { getRowItemsWithActions } from '../../../../common/components/tables/helpers';
import { HostsType } from '../../store/model';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { getUncommonColumnsCurated } from './columns';
const tableType = hostsModel.HostsTableType.uncommonProcesses;
interface UncommonProcessTableProps {
@ -34,15 +30,6 @@ interface UncommonProcessTableProps {
type: hostsModel.HostsType;
}
export type UncommonProcessTableColumns = [
Columns<HostsUncommonProcessesEdges>,
Columns<HostsUncommonProcessesEdges>,
Columns<HostsUncommonProcessesEdges>,
Columns<HostsUncommonProcessesEdges>,
Columns<HostsUncommonProcessesEdges>,
Columns<HostsUncommonProcessesEdges>
];
const rowItems: ItemsPerRow[] = [
{
text: i18n.ROWS_5,
@ -54,14 +41,6 @@ const rowItems: ItemsPerRow[] = [
},
];
export const getArgs = (args: string[] | null | undefined): string | null => {
if (args != null && args.length !== 0) {
return args.join(' ');
} else {
return null;
}
};
const UncommonProcessTableComponent = React.memo<UncommonProcessTableProps>(
({
data,
@ -140,97 +119,3 @@ UncommonProcessTableComponent.displayName = 'UncommonProcessTableComponent';
export const UncommonProcessTable = React.memo(UncommonProcessTableComponent);
UncommonProcessTable.displayName = 'UncommonProcessTable';
const getUncommonColumns = (): UncommonProcessTableColumns => [
{
name: i18n.NAME,
truncateText: false,
mobileOptions: { show: true },
width: '20%',
render: ({ node }) =>
getRowItemsWithActions({
values: node.process.name,
fieldName: 'process.name',
idPrefix: `uncommon-process-table-${node._id}-processName`,
}),
},
{
align: 'right',
name: i18n.NUMBER_OF_HOSTS,
truncateText: false,
mobileOptions: { show: true },
render: ({ node }) => <>{node.hosts != null ? node.hosts.length : getEmptyValue()}</>,
width: '8%',
},
{
align: 'right',
name: i18n.NUMBER_OF_INSTANCES,
truncateText: false,
mobileOptions: { show: true },
render: ({ node }) => defaultToEmptyTag(node.instances),
width: '8%',
},
{
name: i18n.HOSTS,
truncateText: false,
mobileOptions: { show: true },
width: '25%',
render: ({ node }) =>
getRowItemsWithActions({
values: getHostNames(node.hosts),
fieldName: 'host.name',
idPrefix: `uncommon-process-table-${node._id}-processHost`,
render: (item) => <HostDetailsLink hostName={item} />,
}),
},
{
name: i18n.LAST_COMMAND,
truncateText: false,
mobileOptions: { show: true },
width: '25%',
render: ({ node }) =>
getRowItemsWithActions({
values: node.process != null ? node.process.args : null,
fieldName: 'process.args',
idPrefix: `uncommon-process-table-${node._id}-processArgs`,
render: (item) => <HostDetailsLink hostName={item} />,
displayCount: 1,
}),
},
{
name: i18n.LAST_USER,
truncateText: false,
mobileOptions: { show: true },
render: ({ node }) =>
getRowItemsWithActions({
values: node.user != null ? node.user.name : null,
fieldName: 'user.name',
idPrefix: `uncommon-process-table-${node._id}-processUser`,
}),
},
];
export const getHostNames = (hosts: HostEcs[]): string[] => {
if (hosts != null) {
return hosts
.filter((host) => host.name != null && host.name[0] != null)
.map((host) => (host.name != null && host.name[0] != null ? host.name[0] : ''));
} else {
return [];
}
};
export const getUncommonColumnsCurated = (pageType: HostsType): UncommonProcessTableColumns => {
const columns: UncommonProcessTableColumns = getUncommonColumns();
if (pageType === HostsType.details) {
return [i18n.HOSTS, i18n.NUMBER_OF_HOSTS].reduce<UncommonProcessTableColumns>((acc, name) => {
acc.splice(
acc.findIndex((column) => column.name === name),
1
);
return acc;
}, columns);
} else {
return columns;
}
};