[SIEM] Anomaly UI table changes (#40440) (#40937)

## Summary

- [x] Changed "Score" to "Anomaly Score"
- [x] Changed "Detector" to be "Job Name"
- [x] Added Link from "Job Name" to Anomaly Explorer page
- [x] Aligned text of the tables to be at the top
- [x] Removed the Information I from the table rows but kept it on the Host Details and Network Details
- [x] Added Timestamp to the end of the table
- [x] Moved "Job Name" to be after "Anomaly Score"
- [x] Removed Host Name from Anomalies table when on the Host Details page as it is redundant
- [x] Removed Network Name from Anomalies table when on the Network Details page as it is redundant
- [x] Added anomaly score Default threshold of 50 for the advanced settings page

Advanced setting for default Anomaly Score:
<img width="1225" alt="Screen Shot 2019-07-05 at 6 15 31 PM" src="https://user-images.githubusercontent.com/1151048/60749093-5abd4980-9f52-11e9-9340-08ef8e462c8f.png">

Before Host Overview:
<img width="2192" alt="before-overview-hosts" src="https://user-images.githubusercontent.com/1151048/60746932-23916d00-9f3f-11e9-81fb-e3dba98af160.png">

After Host Overview:
<img width="2186" alt="after-overview-hosts" src="https://user-images.githubusercontent.com/1151048/60746938-2f7d2f00-9f3f-11e9-9a4c-37f5bbc19771.png">

Before Host Details:
<img width="2201" alt="before-host-details" src="https://user-images.githubusercontent.com/1151048/60746961-4f145780-9f3f-11e9-9086-2709b7957221.png">

After Host Details:
<img width="2202" alt="after-host-details" src="https://user-images.githubusercontent.com/1151048/60746969-56d3fc00-9f3f-11e9-9110-5fb46fb398c9.png">


Before Network Overview:
<img width="2199" alt="before-network-overivew" src="https://user-images.githubusercontent.com/1151048/60746954-41f76880-9f3f-11e9-8c75-cc7e6dbde276.png">

After Network Overview:
<img width="2196" alt="after-network-overview" src="https://user-images.githubusercontent.com/1151048/60746957-47ed4980-9f3f-11e9-843a-a2b210347649.png">

Before Network Details:
<img width="2200" alt="before-network-details" src="https://user-images.githubusercontent.com/1151048/60746972-5d627380-9f3f-11e9-8dcb-cc1e1d73c0f9.png">

After Network Details:
<img width="2189" alt="after-network-details" src="https://user-images.githubusercontent.com/1151048/60746974-63585480-9f3f-11e9-9847-4645a7b1ab1d.png">

### Checklist

Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR.

- [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)
- [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)
- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials
- [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios
- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)

### For maintainers

- [x] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)
~- [ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~
This commit is contained in:
Frank Hassanabad 2019-07-11 20:25:53 -06:00 committed by GitHub
parent 2b2c8a32e1
commit d5fb64570b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 484 additions and 116 deletions

View file

@ -7,3 +7,4 @@
export const APP_ID = 'siem';
export const APP_NAME = 'SIEM';
export const DEFAULT_INDEX_KEY = 'siem:defaultIndex';
export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore';

View file

@ -11,7 +11,7 @@ import { Server } from 'hapi';
import { initServerWithKibana } from './server/kibana.index';
import { savedObjectMappings } from './server/saved_objects';
import { APP_ID, APP_NAME, DEFAULT_INDEX_KEY } from './common/constants';
import { APP_ID, APP_NAME, DEFAULT_INDEX_KEY, DEFAULT_ANOMALY_SCORE } from './common/constants';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function siem(kibana: any) {
@ -56,6 +56,19 @@ export function siem(kibana: any) {
category: ['siem'],
requiresPageReload: true,
},
[DEFAULT_ANOMALY_SCORE]: {
name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', {
defaultMessage: 'Default anomaly threshold',
}),
value: 50,
type: 'number',
description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', {
defaultMessage:
'Default anomaly score threshold to exceed before showing anomalies. Valid values are between 0 and 100',
}),
category: ['siem'],
requiresPageReload: true,
},
},
mappings: savedObjectMappings,
},

View file

@ -5,7 +5,8 @@
*/
import { InfluencerInput } from '../types';
import { influencersToString } from './use_anomalies_table_data';
import { influencersToString, getThreshold } from './use_anomalies_table_data';
import { AppKibanaFrameworkAdapter } from '../../../lib/adapters/framework/kibana_framework_adapter';
describe('use_anomalies_table_data', () => {
test('should return a reduced single influencer to string', () => {
@ -44,4 +45,46 @@ describe('use_anomalies_table_data', () => {
const influencerString = influencersToString(null);
expect(influencerString).toEqual('');
});
describe('#getThreshold', () => {
test('should return 0 if given something below -1', () => {
const config: Partial<AppKibanaFrameworkAdapter> = {
anomalyScore: -100,
};
expect(getThreshold(config, -1)).toEqual(0);
});
test('should return 100 if given something above 100', () => {
const config: Partial<AppKibanaFrameworkAdapter> = {
anomalyScore: 1000,
};
expect(getThreshold(config, -1)).toEqual(100);
});
test('should return overridden value if passed in as non negative 1', () => {
const config: Partial<AppKibanaFrameworkAdapter> = {
anomalyScore: 75,
};
expect(getThreshold(config, 50)).toEqual(50);
});
test('should return 50 if no anomalyScore was set', () => {
const config: Partial<AppKibanaFrameworkAdapter> = {};
expect(getThreshold(config, -1)).toEqual(50);
});
test('should return custom setting', () => {
const config: Partial<AppKibanaFrameworkAdapter> = {
anomalyScore: 75,
};
expect(getThreshold(config, -1)).toEqual(75);
});
test('should round down a value up if sent in a floating point number', () => {
const config: Partial<AppKibanaFrameworkAdapter> = {
anomalyScore: 75.01,
};
expect(getThreshold(config, -1)).toEqual(75);
});
});
});

View file

@ -40,31 +40,48 @@ export const getTimeZone = (config: Partial<AppKibanaFrameworkAdapter>): string
}
};
export const getThreshold = (
config: Partial<AppKibanaFrameworkAdapter>,
threshold: number
): number => {
if (threshold !== -1) {
return threshold;
} else if (config.anomalyScore == null) {
return 50;
} else if (config.anomalyScore < 0) {
return 0;
} else if (config.anomalyScore > 100) {
return 100;
} else {
return Math.floor(config.anomalyScore);
}
};
export const useAnomaliesTableData = ({
influencers,
startDate,
endDate,
threshold = 0,
threshold = -1,
skip = false,
}: Args): Return => {
const [tableData, setTableData] = useState<Anomalies | null>(null);
const [loading, setLoading] = useState(true);
const config = useContext(KibanaConfigContext);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const fetchFunc = async (
influencersInput: InfluencerInput[] | null,
earliestMs: number,
latestMs: number
) => {
const userPermissions = hasMlUserPermissions(capabilities);
if (userPermissions && influencersInput != null && !skip) {
const data = await anomaliesTableData(
{
jobIds: [],
criteriaFields: [],
aggregationInterval: 'auto',
threshold,
threshold: getThreshold(config, threshold),
earliestMs,
latestMs,
influencers: influencersInput,
@ -89,7 +106,7 @@ export const useAnomaliesTableData = ({
useEffect(() => {
setLoading(true);
fetchFunc(influencers, startDate, endDate);
}, [influencersToString(influencers), startDate, endDate, skip]);
}, [influencersToString(influencers), startDate, endDate, skip, userPermissions]);
return [loading, tableData];
};

View file

@ -21,3 +21,25 @@ exports[`draggable_score renders correctly against snapshot 1`] = `
render={[Function]}
/>
`;
exports[`draggable_score renders correctly against snapshot when the index is not included 1`] = `
<Connect(DraggableWrapperComponent)
dataProvider={
Object {
"and": Array [],
"enabled": true,
"excluded": false,
"id": "some-id",
"kqlQuery": "",
"name": "process.name",
"queryMatch": Object {
"field": "process.name",
"operator": ":",
"value": "du",
},
}
}
key="some-id"
render={[Function]}
/>
`;

View file

@ -8,7 +8,7 @@ import { EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic
import React from 'react';
import styled from 'styled-components';
import { Anomaly, NarrowDateRange } from '../types';
import { getScoreString } from './get_score_string';
import { getScoreString } from './score_health';
import { PreferenceFormattedDate } from '../../formatted_date';
import { createInfluencers } from './../influencers/create_influencers';
import { DescriptionList } from '../../../../common/utility_types';

View file

@ -24,4 +24,9 @@ describe('draggable_score', () => {
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('renders correctly against snapshot when the index is not included', () => {
const wrapper = shallow(<DraggableScore id="some-id" score={anomalies.anomalies[0]} />);
expect(toJson(wrapper)).toMatchSnapshot();
});
});

View file

@ -10,14 +10,14 @@ import { Anomaly } from '../types';
import { IS_OPERATOR } from '../../timeline/data_providers/data_provider';
import { Provider } from '../../timeline/data_providers/provider';
import { Spacer } from '../../page';
import { getScoreString } from './get_score_string';
import { getScoreString } from './score_health';
export const DraggableScore = React.memo<{
id: string;
index: number;
index?: number;
score: Anomaly;
}>(
({ id, index, score }): JSX.Element => (
({ id, index = 0, score }): JSX.Element => (
<DraggableWrapper
key={id}
dataProvider={{

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getScoreString } from './get_score_string';
import { getScoreString } from './score_health';
describe('create_influencers', () => {
test('it rounds up to 1 from 0.3', () => {

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { EuiHealth } from '@elastic/eui';
interface Props {
score: number;
}
export const getScoreString = (score: number) => String(Math.ceil(score));
export const ScoreHealth = React.memo<Props>(({ score }) => {
const scoreCeiling = getScoreString(score);
const color = getSeverityColor(score);
return <EuiHealth color={color}>{scoreCeiling}</EuiHealth>;
});
// ಠ_ಠ A hard-fork of the `ml` ml/common/util/anomaly_utils.js#getSeverityColor ಠ_ಠ
//
// Returns a severity label (one of critical, major, minor, warning, low or unknown)
// for the supplied normalized anomaly score (a value between 0 and 100), where scores
// less than 3 are assigned a severity of 'low'.
export const getSeverityColor = (normalizedScore: number): string => {
if (normalizedScore >= 75) {
return '#fe5050';
} else if (normalizedScore >= 50) {
return '#fba740';
} else if (normalizedScore >= 25) {
return '#fdec25';
} else if (normalizedScore >= 3) {
return '#8bc8fb';
} else if (normalizedScore >= 0) {
return '#d2e9f7';
} else {
return '#ffffff';
}
};

View file

@ -5,26 +5,22 @@
*/
import React, { useContext } from 'react';
import { EuiInMemoryTable, EuiPanel } from '@elastic/eui';
import styled from 'styled-components';
import { EuiPanel } from '@elastic/eui';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HeaderPanel } from '../../header_panel';
import * as i18n from './translations';
import { getAnomaliesHostTableColumns } from './get_anomalies_host_table_columns';
import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns';
import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts';
import { BackgroundRefetch } from '../../load_more_table';
import { BackgroundRefetch, BasicTableContainer } from '../../load_more_table';
import { LoadingPanel } from '../../loading';
import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies';
import { getSizeFromAnomalies } from '../anomaly/get_size_from_anomalies';
import { dateTimesAreEqual } from './date_time_equality';
import { AnomaliesTableProps } from '../types';
import { AnomaliesHostTableProps } from '../types';
import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider';
const BasicTableContainer = styled.div`
position: relative;
`;
import { BasicTable } from './basic_table';
const sorting = {
sort: {
@ -33,20 +29,25 @@ const sorting = {
},
};
export const AnomaliesHostTable = React.memo<AnomaliesTableProps>(
({ startDate, endDate, narrowDateRange, hostName, skip }): JSX.Element | null => {
export const AnomaliesHostTable = React.memo<AnomaliesHostTableProps>(
({ startDate, endDate, narrowDateRange, hostName, skip, type }): JSX.Element | null => {
const capabilities = useContext(MlCapabilitiesContext);
const [loading, tableData] = useAnomaliesTableData({
influencers: [],
startDate,
endDate,
threshold: 0,
skip,
});
const hosts = convertAnomaliesToHosts(tableData, hostName);
const interval = getIntervalFromAnomalies(tableData);
const columns = getAnomaliesHostTableColumns(startDate, endDate, interval, narrowDateRange);
const columns = getAnomaliesHostTableColumnsCurated(
type,
startDate,
endDate,
interval,
narrowDateRange
);
const pagination = {
pageIndex: 0,
pageSize: 10,
@ -77,12 +78,7 @@ export const AnomaliesHostTable = React.memo<AnomaliesTableProps>(
subtitle={`${i18n.SHOWING}: ${hosts.length.toLocaleString()} ${i18n.ANOMALIES}`}
title={i18n.ANOMALIES}
/>
<EuiInMemoryTable
items={hosts}
columns={columns}
pagination={pagination}
sorting={sorting}
/>
<BasicTable items={hosts} columns={columns} pagination={pagination} sorting={sorting} />
</BasicTableContainer>
</EuiPanel>
);

View file

@ -5,26 +5,22 @@
*/
import React, { useContext } from 'react';
import { EuiInMemoryTable, EuiPanel } from '@elastic/eui';
import styled from 'styled-components';
import { EuiPanel } from '@elastic/eui';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HeaderPanel } from '../../header_panel';
import * as i18n from './translations';
import { convertAnomaliesToNetwork } from './convert_anomalies_to_network';
import { BackgroundRefetch } from '../../load_more_table';
import { BackgroundRefetch, BasicTableContainer } from '../../load_more_table';
import { LoadingPanel } from '../../loading';
import { AnomaliesTableProps } from '../types';
import { getAnomaliesNetworkTableColumns } from './get_anomalies_network_table_columns';
import { AnomaliesNetworkTableProps } from '../types';
import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns';
import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies';
import { getSizeFromAnomalies } from '../anomaly/get_size_from_anomalies';
import { dateTimesAreEqual } from './date_time_equality';
import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider';
const BasicTableContainer = styled.div`
position: relative;
`;
import { BasicTable } from './basic_table';
const sorting = {
sort: {
@ -33,20 +29,25 @@ const sorting = {
},
};
export const AnomaliesNetworkTable = React.memo<AnomaliesTableProps>(
({ startDate, endDate, narrowDateRange, skip, ip }): JSX.Element | null => {
export const AnomaliesNetworkTable = React.memo<AnomaliesNetworkTableProps>(
({ startDate, endDate, narrowDateRange, skip, ip, type }): JSX.Element | null => {
const capabilities = useContext(MlCapabilitiesContext);
const [loading, tableData] = useAnomaliesTableData({
influencers: [],
startDate,
endDate,
threshold: 0,
skip,
});
const networks = convertAnomaliesToNetwork(tableData, ip);
const interval = getIntervalFromAnomalies(tableData);
const columns = getAnomaliesNetworkTableColumns(startDate, endDate, interval, narrowDateRange);
const columns = getAnomaliesNetworkTableColumnsCurated(
type,
startDate,
endDate,
interval,
narrowDateRange
);
const pagination = {
pageIndex: 0,
pageSize: 10,
@ -77,7 +78,7 @@ export const AnomaliesNetworkTable = React.memo<AnomaliesTableProps>(
subtitle={`${i18n.SHOWING}: ${networks.length.toLocaleString()} ${i18n.ANOMALIES}`}
title={i18n.ANOMALIES}
/>
<EuiInMemoryTable
<BasicTable
items={networks}
columns={columns}
pagination={pagination}

View file

@ -4,4 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
export const getScoreString = (score: number): string => String(Math.ceil(score));
import styled from 'styled-components';
import { EuiInMemoryTable } from '@elastic/eui';
export const BasicTable = styled(EuiInMemoryTable)`
tbody {
th,
td {
vertical-align: top;
}
}
`;

View file

@ -6,10 +6,8 @@
import { AnomaliesByHost, AnomaliesByNetwork } from '../types';
export const createCompoundHostKey = (anomaliesByHost: AnomaliesByHost): string => {
return `${anomaliesByHost.hostName}-${anomaliesByHost.anomaly.entityName}-${anomaliesByHost.anomaly.entityValue}-${anomaliesByHost.anomaly.severity}-${anomaliesByHost.anomaly.jobId}`;
};
export const createCompoundHostKey = (anomaliesByHost: AnomaliesByHost): string =>
`${anomaliesByHost.hostName}-${anomaliesByHost.anomaly.entityName}-${anomaliesByHost.anomaly.entityValue}-${anomaliesByHost.anomaly.severity}-${anomaliesByHost.anomaly.jobId}`;
export const createCompoundNetworkKey = (anomaliesByNetwork: AnomaliesByNetwork): string => {
return `${anomaliesByNetwork.ip}-${anomaliesByNetwork.anomaly.entityName}-${anomaliesByNetwork.anomaly.entityValue}-${anomaliesByNetwork.anomaly.severity}-${anomaliesByNetwork.anomaly.jobId}`;
};
export const createCompoundNetworkKey = (anomaliesByNetwork: AnomaliesByNetwork): string =>
`${anomaliesByNetwork.ip}-${anomaliesByNetwork.anomaly.entityName}-${anomaliesByNetwork.anomaly.entityValue}-${anomaliesByNetwork.anomaly.severity}-${anomaliesByNetwork.anomaly.jobId}`;

View file

@ -5,17 +5,17 @@
*/
import { dateTimesAreEqual } from './date_time_equality';
import { AnomaliesTableProps } from '../types';
import { HostOrNetworkProps } from '../types';
describe('date_time_equality', () => {
test('it returns true if start and end date are equal', () => {
const prev: AnomaliesTableProps = {
const prev: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
skip: false,
};
const next: AnomaliesTableProps = {
const next: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
@ -26,13 +26,13 @@ describe('date_time_equality', () => {
});
test('it returns false if starts are not equal', () => {
const prev: AnomaliesTableProps = {
const prev: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('1999').valueOf(),
narrowDateRange: jest.fn(),
skip: false,
};
const next: AnomaliesTableProps = {
const next: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
@ -43,13 +43,13 @@ describe('date_time_equality', () => {
});
test('it returns false if starts are not equal for next', () => {
const prev: AnomaliesTableProps = {
const prev: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
skip: false,
};
const next: AnomaliesTableProps = {
const next: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('1999').valueOf(),
narrowDateRange: jest.fn(),
@ -60,13 +60,13 @@ describe('date_time_equality', () => {
});
test('it returns false if ends are not equal', () => {
const prev: AnomaliesTableProps = {
const prev: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2001').valueOf(),
narrowDateRange: jest.fn(),
skip: false,
};
const next: AnomaliesTableProps = {
const next: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
@ -77,13 +77,13 @@ describe('date_time_equality', () => {
});
test('it returns false if ends are not equal for next', () => {
const prev: AnomaliesTableProps = {
const prev: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
skip: false,
};
const next: AnomaliesTableProps = {
const next: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2001').valueOf(),
narrowDateRange: jest.fn(),
@ -94,13 +94,13 @@ describe('date_time_equality', () => {
});
test('it returns false if skip is not equal', () => {
const prev: AnomaliesTableProps = {
const prev: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),
skip: true,
};
const next: AnomaliesTableProps = {
const next: HostOrNetworkProps = {
startDate: new Date('2000').valueOf(),
endDate: new Date('2000').valueOf(),
narrowDateRange: jest.fn(),

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AnomaliesTableProps } from '../types';
import { HostOrNetworkProps } from '../types';
export const dateTimesAreEqual = (
prevProps: AnomaliesTableProps,
nextProps: AnomaliesTableProps
prevProps: HostOrNetworkProps,
nextProps: HostOrNetworkProps
): boolean =>
prevProps.startDate === nextProps.startDate &&
prevProps.endDate === nextProps.endDate &&

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns';
import { HostsType } from '../../../store/hosts/model';
import * as i18n from './translations';
const startDate = new Date(2001).valueOf();
const endDate = new Date(3000).valueOf();
const interval = 'days';
const narrowDateRange = jest.fn();
describe('get_anomalies_host_table_columns', () => {
test('on hosts page, we expect to get all columns', () => {
expect(
getAnomaliesHostTableColumnsCurated(
HostsType.page,
startDate,
endDate,
interval,
narrowDateRange
).length
).toEqual(6);
});
test('on host details page, we expect to remove one columns', () => {
const columns = getAnomaliesHostTableColumnsCurated(
HostsType.details,
startDate,
endDate,
interval,
narrowDateRange
);
expect(columns.length).toEqual(5);
});
test('on host page, we should have Host Name', () => {
const columns = getAnomaliesHostTableColumnsCurated(
HostsType.page,
startDate,
endDate,
interval,
narrowDateRange
);
expect(columns.some(col => col.name === i18n.HOST_NAME)).toEqual(true);
});
test('on host details page, we should not have Host Name', () => {
const columns = getAnomaliesHostTableColumnsCurated(
HostsType.details,
startDate,
endDate,
interval,
narrowDateRange
);
expect(columns.some(col => col.name === i18n.HOST_NAME)).toEqual(false);
});
});

View file

@ -5,7 +5,8 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import moment from 'moment';
import { Columns } from '../../load_more_table';
import { AnomaliesByHost, Anomaly, NarrowDateRange } from '../types';
import { getRowItemDraggable } from '../../tables/helpers';
@ -14,8 +15,12 @@ import { createCompoundHostKey } from './create_compound_key';
import { HostDetailsLink } from '../../links';
import * as i18n from './translations';
import { AnomalyScore } from '../score/anomaly_score';
import { getEntries } from '../get_entries';
import { DraggableScore } from '../score/draggable_score';
import { createExplorerLink } from '../links/create_explorer_link';
import { LocalizedDateTooltip } from '../../localized_date_tooltip';
import { PreferenceFormattedDate } from '../../formatted_date';
import { HostsType } from '../../../store/hosts/model';
export const getAnomaliesHostTableColumns = (
startDate: number,
@ -25,9 +30,10 @@ export const getAnomaliesHostTableColumns = (
): [
Columns<AnomaliesByHost['hostName'], AnomaliesByHost>,
Columns<Anomaly['severity'], AnomaliesByHost>,
Columns<Anomaly['jobId'], AnomaliesByHost>,
Columns<Anomaly['entityValue'], AnomaliesByHost>,
Columns<Anomaly['influencers'], AnomaliesByHost>,
Columns<Anomaly['jobId']>
Columns<Anomaly['time'], AnomaliesByHost>
] => [
{
name: i18n.HOST_NAME,
@ -43,17 +49,26 @@ export const getAnomaliesHostTableColumns = (
render: item => <HostDetailsLink hostName={item} />,
}),
},
{
name: i18n.DETECTOR,
field: 'anomaly.jobId',
sortable: true,
render: (jobId, anomaliesByHost) => (
<EuiLink
href={`${createExplorerLink(anomaliesByHost.anomaly, startDate, endDate)}`}
target="_blank"
>
{jobId}
</EuiLink>
),
},
{
name: i18n.SCORE,
field: 'anomaly.severity',
sortable: true,
render: (_, anomaliesByHost) => (
<AnomalyScore
startDate={startDate}
endDate={endDate}
jobKey={`anomalies-host-table-severity-${createCompoundHostKey(anomaliesByHost)}`}
narrowDateRange={narrowDateRange}
interval={interval}
<DraggableScore
id={`anomalies-host-table-severity-${createCompoundHostKey(anomaliesByHost)}`}
score={anomaliesByHost.anomaly}
/>
),
@ -104,8 +119,30 @@ export const getAnomaliesHostTableColumns = (
),
},
{
name: i18n.DETECTOR,
field: 'anomaly.jobId',
name: i18n.TIME_STAMP,
field: 'anomaly.time',
sortable: true,
render: time => (
<LocalizedDateTooltip date={moment(new Date(time)).toDate()}>
<PreferenceFormattedDate value={new Date(time)} />
</LocalizedDateTooltip>
),
},
];
export const getAnomaliesHostTableColumnsCurated = (
pageType: HostsType,
startDate: number,
endDate: number,
interval: string,
narrowDateRange: NarrowDateRange
) => {
const columns = getAnomaliesHostTableColumns(startDate, endDate, interval, narrowDateRange);
// Columns to exclude from host details pages
if (pageType === 'details') {
return columns.filter(column => column.name !== i18n.HOST_NAME);
} else {
return columns;
}
};

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns';
import { NetworkType } from '../../../store/network/model';
import * as i18n from './translations';
const startDate = new Date(2001).valueOf();
const endDate = new Date(3000).valueOf();
const interval = 'days';
const narrowDateRange = jest.fn();
describe('get_anomalies_network_table_columns', () => {
test('on network page, we expect to get all columns', () => {
expect(
getAnomaliesNetworkTableColumnsCurated(
NetworkType.page,
startDate,
endDate,
interval,
narrowDateRange
).length
).toEqual(6);
});
test('on network details page, we expect to remove one columns', () => {
const columns = getAnomaliesNetworkTableColumnsCurated(
NetworkType.details,
startDate,
endDate,
interval,
narrowDateRange
);
expect(columns.length).toEqual(5);
});
test('on network page, we should have Network Name', () => {
const columns = getAnomaliesNetworkTableColumnsCurated(
NetworkType.page,
startDate,
endDate,
interval,
narrowDateRange
);
expect(columns.some(col => col.name === i18n.NETWORK_NAME)).toEqual(true);
});
test('on network details page, we should not have Network Name', () => {
const columns = getAnomaliesNetworkTableColumnsCurated(
NetworkType.details,
startDate,
endDate,
interval,
narrowDateRange
);
expect(columns.some(col => col.name === i18n.NETWORK_NAME)).toEqual(false);
});
});

View file

@ -5,7 +5,8 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import moment from 'moment';
import { Columns } from '../../load_more_table';
import { Anomaly, NarrowDateRange, AnomaliesByNetwork } from '../types';
import { getRowItemDraggable } from '../../tables/helpers';
@ -14,8 +15,12 @@ import { createCompoundNetworkKey } from './create_compound_key';
import { IPDetailsLink } from '../../links';
import * as i18n from './translations';
import { AnomalyScore } from '../score/anomaly_score';
import { getEntries } from '../get_entries';
import { DraggableScore } from '../score/draggable_score';
import { createExplorerLink } from '../links/create_explorer_link';
import { LocalizedDateTooltip } from '../../localized_date_tooltip';
import { PreferenceFormattedDate } from '../../formatted_date';
import { NetworkType } from '../../../store/network/model';
export const getAnomaliesNetworkTableColumns = (
startDate: number,
@ -25,9 +30,10 @@ export const getAnomaliesNetworkTableColumns = (
): [
Columns<AnomaliesByNetwork['ip'], AnomaliesByNetwork>,
Columns<Anomaly['severity'], AnomaliesByNetwork>,
Columns<Anomaly['jobId'], AnomaliesByNetwork>,
Columns<Anomaly['entityValue'], AnomaliesByNetwork>,
Columns<Anomaly['influencers'], AnomaliesByNetwork>,
Columns<Anomaly['jobId']>
Columns<Anomaly['time'], AnomaliesByNetwork>
] => [
{
name: i18n.NETWORK_NAME,
@ -41,17 +47,26 @@ export const getAnomaliesNetworkTableColumns = (
render: item => <IPDetailsLink ip={item} />,
}),
},
{
name: i18n.DETECTOR,
field: 'anomaly.jobId',
sortable: true,
render: (jobId, anomaliesByHost) => (
<EuiLink
href={`${createExplorerLink(anomaliesByHost.anomaly, startDate, endDate)}`}
target="_blank"
>
{jobId}
</EuiLink>
),
},
{
name: i18n.SCORE,
field: 'anomaly.severity',
sortable: true,
render: (_, anomaliesByNetwork) => (
<AnomalyScore
startDate={startDate}
endDate={endDate}
jobKey={`anomalies-network-table-severity-${createCompoundNetworkKey(anomaliesByNetwork)}`}
narrowDateRange={narrowDateRange}
interval={interval}
<DraggableScore
id={`anomalies-network-table-severity-${createCompoundNetworkKey(anomaliesByNetwork)}`}
score={anomaliesByNetwork.anomaly}
/>
),
@ -98,8 +113,30 @@ export const getAnomaliesNetworkTableColumns = (
),
},
{
name: i18n.DETECTOR,
field: 'anomaly.jobId',
name: i18n.TIME_STAMP,
field: 'anomaly.time',
sortable: true,
render: time => (
<LocalizedDateTooltip date={moment(new Date(time)).toDate()}>
<PreferenceFormattedDate value={new Date(time)} />
</LocalizedDateTooltip>
),
},
];
export const getAnomaliesNetworkTableColumnsCurated = (
pageType: NetworkType,
startDate: number,
endDate: number,
interval: string,
narrowDateRange: NarrowDateRange
) => {
const columns = getAnomaliesNetworkTableColumns(startDate, endDate, interval, narrowDateRange);
// Columns to exclude from ip details pages
if (pageType === 'details') {
return columns.filter(column => column.name !== i18n.NETWORK_NAME);
} else {
return columns;
}
};

View file

@ -19,7 +19,7 @@ export const LOADING = i18n.translate('xpack.siem.ml.table.loadingDescription',
});
export const SCORE = i18n.translate('xpack.siem.ml.table.scoreTitle', {
defaultMessage: 'Score',
defaultMessage: 'Anomaly Score',
});
export const HOST_NAME = i18n.translate('xpack.siem.ml.table.hostNameTitle', {
@ -35,9 +35,13 @@ export const ENTITY = i18n.translate('xpack.siem.ml.table.entityTitle', {
});
export const DETECTOR = i18n.translate('xpack.siem.ml.table.detectorTitle', {
defaultMessage: 'Detector',
defaultMessage: 'Job Name',
});
export const NETWORK_NAME = i18n.translate('xpack.siem.ml.table.networkNameTitle', {
defaultMessage: 'Network IP',
});
export const TIME_STAMP = i18n.translate('xpack.siem.ml.table.timestampTitle', {
defaultMessage: 'Timestamp',
});

View file

@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostsType } from '../../store/hosts/model';
import { NetworkType } from '../../store/network/model';
export interface Influencer {
influencer_field_name: string;
influencer_field_values: string[];
@ -73,15 +76,23 @@ export interface AnomaliesByNetwork {
anomaly: Anomaly;
}
export interface AnomaliesTableProps {
export interface HostOrNetworkProps {
startDate: number;
endDate: number;
narrowDateRange: NarrowDateRange;
skip: boolean;
hostName?: string;
ip?: string;
}
export type AnomaliesHostTableProps = HostOrNetworkProps & {
hostName?: string;
type: HostsType;
};
export type AnomaliesNetworkTableProps = HostOrNetworkProps & {
ip?: string;
type: NetworkType;
};
export interface MlCapabilities {
capabilities: {
canGetJobs: boolean;

View file

@ -23,10 +23,10 @@ export const useJobSummaryData = (jobIds: string[], refetchSummaryData = false):
const [loading, setLoading] = useState(true);
const config = useContext(KibanaConfigContext);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const fetchFunc = async () => {
if (jobIds.length > 0) {
const userPermissions = hasMlUserPermissions(capabilities);
if (userPermissions) {
const data: Job[] = await jobsSummary(jobIds, {
'kbn-version': config.kbnVersion,
@ -44,7 +44,7 @@ export const useJobSummaryData = (jobIds: string[], refetchSummaryData = false):
useEffect(() => {
setLoading(true);
fetchFunc();
}, [jobIds.join(','), refetchSummaryData]);
}, [jobIds.join(','), refetchSummaryData, userPermissions]);
return [loading, jobSummaryData];
};

View file

@ -23,9 +23,9 @@ export const useSiemJobs = (refetchData: boolean): Return => {
const [loading, setLoading] = useState(true);
const config = useContext(KibanaConfigContext);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const fetchFunc = async () => {
const userPermissions = hasMlUserPermissions(capabilities);
if (userPermissions) {
const data = await groupsData({
'kbn-version': config.kbnVersion,
@ -41,7 +41,7 @@ export const useSiemJobs = (refetchData: boolean): Return => {
useEffect(() => {
setLoading(true);
fetchFunc();
}, [refetchData]);
}, [refetchData, userPermissions]);
return [loading, siemJobs];
};

View file

@ -11,9 +11,7 @@ import * as React from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { apolloClientObservable, mockGlobalState } from '../../../../mock';
import { AuthenticationsEdges } from '../../../../graphql/types';
import { createStore, hostsModel, State } from '../../../../store';
import { Columns } from '../../../load_more_table';
import { mockData } from './mock';
import * as i18n from './translations';
@ -60,22 +58,24 @@ describe('Authentication Table Component', () => {
expect(columns.length).toEqual(7);
});
test('on host details page, we should have Last Failed Destination column', () => {
const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page);
expect(columns.some(col => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(true);
});
test('on host details page, we should not have Last Failed Destination column', () => {
const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details);
expect(
columns.includes(
(col: Columns<AuthenticationsEdges>) => col.name === i18n.LAST_FAILED_DESTINATION
)
).toEqual(false);
expect(columns.some(col => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(false);
});
test('on host page, we should have Last Successful Destination column', () => {
const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page);
expect(columns.some(col => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(true);
});
test('on host details page, we should not have Last Successful Destination column', () => {
const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details);
expect(
columns.includes(
(col: Columns<AuthenticationsEdges>) => col.name === i18n.LAST_SUCCESSFUL_DESTINATION
)
).toEqual(false);
expect(columns.some(col => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(false);
});
});
});

View file

@ -11,9 +11,7 @@ import * as React from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { apolloClientObservable, mockGlobalState } from '../../../../mock';
import { EcsEdges } from '../../../../graphql/types';
import { createStore, hostsModel, State } from '../../../../store';
import { Columns } from '../../../load_more_table';
import { EventsTable, getEventsColumnsCurated } from '.';
import { mockData } from './mock';
@ -61,11 +59,14 @@ describe('Load More Events Table Component', () => {
expect(columns.length).toEqual(7);
});
test('on host page, we should have Host Name column', () => {
const columns = getEventsColumnsCurated(hostsModel.HostsType.page);
expect(columns.some(col => col.name === i18n.HOST_NAME)).toEqual(true);
});
test('on host details page, we should not have Host Name column', () => {
const columns = getEventsColumnsCurated(hostsModel.HostsType.details);
expect(columns.includes((col: Columns<EcsEdges>) => col.name === i18n.HOST_NAME)).toEqual(
false
);
expect(columns.some(col => col.name === i18n.HOST_NAME)).toEqual(false);
});
});
});

View file

@ -11,6 +11,7 @@ import * as ReactDOM from 'react-dom';
import { UIRoutes as KibanaUIRoutes } from 'ui/routes';
import { DEFAULT_INDEX_KEY, DEFAULT_ANOMALY_SCORE } from '../../../../common/constants';
import {
AppBufferedKibanaServiceCall,
AppFrameworkAdapter,
@ -31,6 +32,7 @@ export class AppKibanaFrameworkAdapter implements AppFrameworkAdapter {
public dateFormatTz?: string;
public darkMode?: boolean;
public indexPattern?: string;
public anomalyScore?: number;
public kbnVersion?: string;
public scaledDateFormat?: string;
public timezone?: string;
@ -143,7 +145,8 @@ export class AppKibanaFrameworkAdapter implements AppFrameworkAdapter {
} catch (e) {
this.darkMode = false;
}
this.indexPattern = config.get('siem:defaultIndex');
this.indexPattern = config.get(DEFAULT_INDEX_KEY);
this.anomalyScore = config.get(DEFAULT_ANOMALY_SCORE);
this.scaledDateFormat = config.get('dateFormat:scaled');
});

View file

@ -12,6 +12,7 @@ export class AppTestingFrameworkAdapter implements AppFrameworkAdapter {
public dateFormat?: string;
public dateFormatTz?: string;
public indexPattern?: string;
public anomalyScore?: number;
public kbnVersion?: string;
public scaledDateFormat?: string;
public timezone?: string;

View file

@ -27,6 +27,7 @@ export interface AppFrameworkAdapter {
dateFormatTz?: string;
darkMode?: boolean;
indexPattern?: string;
anomalyScore?: number;
kbnVersion?: string;
scaledDateFormat?: string;
timezone?: string;

View file

@ -233,6 +233,7 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
endDate={to}
skip={isInitializing}
hostName={hostName}
type={type}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({

View file

@ -214,6 +214,7 @@ const HostsComponent = pure<HostsComponentProps>(({ filterQuery, setAbsoluteRang
startDate={from}
endDate={to}
skip={isInitializing}
type={hostsModel.HostsType.page}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({

View file

@ -260,6 +260,7 @@ export const IPDetailsComponent = pure<IPDetailsComponentProps>(
endDate={to}
skip={isInitializing}
ip={ip}
type={networkModel.NetworkType.details}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({

View file

@ -168,6 +168,7 @@ const NetworkComponent = pure<NetworkComponentProps>(
startDate={from}
endDate={to}
skip={isInitializing}
type={networkModel.NetworkType.page}
narrowDateRange={(score, interval) => {
const fromTo = scoreIntervalToDateTime(score, interval);
setAbsoluteRangeDatePicker({