[Security Solution] Alerts details (#83963)
* init alert details tab * styles * readMore button * readmore btn * field mappings * add unit tests * unit test * fix unit test * functional test * isolate lineClamp component * review * unit test * fix rule name in events table * originalvalue * unit test * add close event details button * rollback cypress configs * cypress * close events details * remove Ip * review * review * review * review * review * review * review * fix i18n check * fix import * fix eslint * use connect * close flyout when expanded event doesn't exist in the list * Update x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com> * fix types * unit test * fix rule status badge * isolate host name renderer * fixup * cypress * cypress * defaultModel * review comments * unit test * replace findIndex with some * review * remove defaultModel from toggle event action * review * cleanup defaultModel * unit test * rollback handleClearSelection * fixup * fix i18n * cleanup defaultmodel * cleanup * summary value * fix showing timeline details * layout * fix timeline memoization * fix long query * styling Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
parent
f28a80fd29
commit
7b32835226
|
@ -9,6 +9,7 @@ import { Inspect, Maybe } from '../../../common';
|
|||
import { TimelineRequestOptionsPaginated } from '../..';
|
||||
|
||||
export interface TimelineEventsDetailsItem {
|
||||
category?: string;
|
||||
field: string;
|
||||
values?: Maybe<string[]>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
@ -411,7 +411,6 @@ export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom';
|
|||
export interface TimelineExpandedEventType {
|
||||
eventId: string;
|
||||
indexName: string;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
@ -4,14 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ALERT_ID } from '../screens/alerts';
|
||||
import { PROVIDER_BADGE } from '../screens/timeline';
|
||||
|
||||
import {
|
||||
expandFirstAlert,
|
||||
investigateFirstAlertInTimeline,
|
||||
waitForAlertsPanelToBeLoaded,
|
||||
} from '../tasks/alerts';
|
||||
import { investigateFirstAlertInTimeline, waitForAlertsPanelToBeLoaded } from '../tasks/alerts';
|
||||
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
|
||||
import { loginAndWaitForPage } from '../tasks/login';
|
||||
|
||||
|
@ -29,13 +24,13 @@ describe('Alerts timeline', () => {
|
|||
|
||||
it('Investigate alert in default timeline', () => {
|
||||
waitForAlertsPanelToBeLoaded();
|
||||
expandFirstAlert();
|
||||
cy.get(ALERT_ID)
|
||||
investigateFirstAlertInTimeline();
|
||||
cy.get(PROVIDER_BADGE)
|
||||
.first()
|
||||
.invoke('text')
|
||||
.then((eventId) => {
|
||||
investigateFirstAlertInTimeline();
|
||||
cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', `_id: "${eventId}"`);
|
||||
cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -352,7 +352,6 @@ export const CaseComponent = React.memo<CaseProps>(
|
|||
event: {
|
||||
eventId: alertId,
|
||||
indexName: index,
|
||||
loading: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -0,0 +1,657 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const mockAlertDetailsData = [
|
||||
{ category: 'process', field: 'process.name', values: ['-'], originalValue: '-' },
|
||||
{ category: 'process', field: 'process.pid', values: [0], originalValue: 0 },
|
||||
{ category: 'process', field: 'process.executable', values: ['-'], originalValue: '-' },
|
||||
{
|
||||
category: 'agent',
|
||||
field: 'agent.hostname',
|
||||
values: ['windows-native'],
|
||||
originalValue: 'windows-native',
|
||||
},
|
||||
{
|
||||
category: 'agent',
|
||||
field: 'agent.name',
|
||||
values: ['windows-native'],
|
||||
originalValue: 'windows-native',
|
||||
},
|
||||
{
|
||||
category: 'agent',
|
||||
field: 'agent.id',
|
||||
values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'],
|
||||
originalValue: 'abfe4a35-d5b4-42a0-a539-bd054c791769',
|
||||
},
|
||||
{ category: 'agent', field: 'agent.type', values: ['winlogbeat'], originalValue: 'winlogbeat' },
|
||||
{
|
||||
category: 'agent',
|
||||
field: 'agent.ephemeral_id',
|
||||
values: ['b9850845-c000-4ddd-bd51-9978a07b7e7d'],
|
||||
originalValue: 'b9850845-c000-4ddd-bd51-9978a07b7e7d',
|
||||
},
|
||||
{ category: 'agent', field: 'agent.version', values: ['7.10.0'], originalValue: '7.10.0' },
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.computer_name',
|
||||
values: ['windows-native'],
|
||||
originalValue: 'windows-native',
|
||||
},
|
||||
{ category: 'winlog', field: 'winlog.process.pid', values: [624], originalValue: 624 },
|
||||
{ category: 'winlog', field: 'winlog.process.thread.id', values: [1896], originalValue: 1896 },
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.keywords',
|
||||
values: ['Audit Failure'],
|
||||
originalValue: ['Audit Failure'],
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.logon.failure.reason',
|
||||
values: ['Unknown user name or bad password.'],
|
||||
originalValue: 'Unknown user name or bad password.',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.logon.failure.sub_status',
|
||||
values: ['User logon with misspelled or bad password'],
|
||||
originalValue: 'User logon with misspelled or bad password',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.logon.failure.status',
|
||||
values: ['This is either due to a bad username or authentication information'],
|
||||
originalValue: 'This is either due to a bad username or authentication information',
|
||||
},
|
||||
{ category: 'winlog', field: 'winlog.logon.id', values: ['0x0'], originalValue: '0x0' },
|
||||
{ category: 'winlog', field: 'winlog.logon.type', values: ['Network'], originalValue: 'Network' },
|
||||
{ category: 'winlog', field: 'winlog.channel', values: ['Security'], originalValue: 'Security' },
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.Status',
|
||||
values: ['0xc000006d'],
|
||||
originalValue: '0xc000006d',
|
||||
},
|
||||
{ category: 'winlog', field: 'winlog.event_data.LogonType', values: ['3'], originalValue: '3' },
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.SubjectLogonId',
|
||||
values: ['0x0'],
|
||||
originalValue: '0x0',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.TransmittedServices',
|
||||
values: ['-'],
|
||||
originalValue: '-',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.LmPackageName',
|
||||
values: ['-'],
|
||||
originalValue: '-',
|
||||
},
|
||||
{ category: 'winlog', field: 'winlog.event_data.KeyLength', values: ['0'], originalValue: '0' },
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.SubjectUserName',
|
||||
values: ['-'],
|
||||
originalValue: '-',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.FailureReason',
|
||||
values: ['%%2313'],
|
||||
originalValue: '%%2313',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.SubjectDomainName',
|
||||
values: ['-'],
|
||||
originalValue: '-',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.TargetUserName',
|
||||
values: ['administrator'],
|
||||
originalValue: 'administrator',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.SubStatus',
|
||||
values: ['0xc000006a'],
|
||||
originalValue: '0xc000006a',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.LogonProcessName',
|
||||
values: ['NtLmSsp '],
|
||||
originalValue: 'NtLmSsp ',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.SubjectUserSid',
|
||||
values: ['S-1-0-0'],
|
||||
originalValue: 'S-1-0-0',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.AuthenticationPackageName',
|
||||
values: ['NTLM'],
|
||||
originalValue: 'NTLM',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.event_data.TargetUserSid',
|
||||
values: ['S-1-0-0'],
|
||||
originalValue: 'S-1-0-0',
|
||||
},
|
||||
{ category: 'winlog', field: 'winlog.opcode', values: ['Info'], originalValue: 'Info' },
|
||||
{ category: 'winlog', field: 'winlog.record_id', values: [890770], originalValue: 890770 },
|
||||
{ category: 'winlog', field: 'winlog.task', values: ['Logon'], originalValue: 'Logon' },
|
||||
{ category: 'winlog', field: 'winlog.event_id', values: [4625], originalValue: 4625 },
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.provider_guid',
|
||||
values: ['{54849625-5478-4994-a5ba-3e3b0328c30d}'],
|
||||
originalValue: '{54849625-5478-4994-a5ba-3e3b0328c30d}',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.activity_id',
|
||||
values: ['{e148a943-f9c4-0001-5a39-81b88bbed601}'],
|
||||
originalValue: '{e148a943-f9c4-0001-5a39-81b88bbed601}',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.api',
|
||||
values: ['wineventlog'],
|
||||
originalValue: 'wineventlog',
|
||||
},
|
||||
{
|
||||
category: 'winlog',
|
||||
field: 'winlog.provider_name',
|
||||
values: ['Microsoft-Windows-Security-Auditing'],
|
||||
originalValue: 'Microsoft-Windows-Security-Auditing',
|
||||
},
|
||||
{ category: 'log', field: 'log.level', values: ['information'], originalValue: 'information' },
|
||||
{ category: 'source', field: 'source.port', values: [0], originalValue: 0 },
|
||||
{ category: 'source', field: 'source.domain', values: ['-'], originalValue: '-' },
|
||||
{
|
||||
category: 'source',
|
||||
field: 'source.ip',
|
||||
values: ['185.156.74.3'],
|
||||
originalValue: '185.156.74.3',
|
||||
},
|
||||
{
|
||||
category: 'base',
|
||||
field: 'message',
|
||||
values: [
|
||||
'An account failed to log on.\n\nSubject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nLogon Type:\t\t\t3\n\nAccount For Which Logon Failed:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\tadministrator\n\tAccount Domain:\t\t\n\nFailure Information:\n\tFailure Reason:\t\tUnknown user name or bad password.\n\tStatus:\t\t\t0xC000006D\n\tSub Status:\t\t0xC000006A\n\nProcess Information:\n\tCaller Process ID:\t0x0\n\tCaller Process Name:\t-\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t185.156.74.3\n\tSource Port:\t\t0\n\nDetailed Authentication Information:\n\tLogon Process:\t\tNtLmSsp \n\tAuthentication Package:\tNTLM\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon request fails. It is generated on the computer where access was attempted.\n\nThe Subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe Logon Type field indicates the kind of logon that was requested. The most common types are 2 (interactive) and 3 (network).\n\nThe Process Information fields indicate which account and process on the system requested the logon.\n\nThe Network Information fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.',
|
||||
],
|
||||
originalValue:
|
||||
'An account failed to log on.\n\nSubject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nLogon Type:\t\t\t3\n\nAccount For Which Logon Failed:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\tadministrator\n\tAccount Domain:\t\t\n\nFailure Information:\n\tFailure Reason:\t\tUnknown user name or bad password.\n\tStatus:\t\t\t0xC000006D\n\tSub Status:\t\t0xC000006A\n\nProcess Information:\n\tCaller Process ID:\t0x0\n\tCaller Process Name:\t-\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t185.156.74.3\n\tSource Port:\t\t0\n\nDetailed Authentication Information:\n\tLogon Process:\t\tNtLmSsp \n\tAuthentication Package:\tNTLM\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon request fails. It is generated on the computer where access was attempted.\n\nThe Subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe Logon Type field indicates the kind of logon that was requested. The most common types are 2 (interactive) and 3 (network).\n\nThe Process Information fields indicate which account and process on the system requested the logon.\n\nThe Network Information fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.',
|
||||
},
|
||||
{
|
||||
category: 'cloud',
|
||||
field: 'cloud.availability_zone',
|
||||
values: ['us-central1-a'],
|
||||
originalValue: 'us-central1-a',
|
||||
},
|
||||
{
|
||||
category: 'cloud',
|
||||
field: 'cloud.instance.name',
|
||||
values: ['windows-native'],
|
||||
originalValue: 'windows-native',
|
||||
},
|
||||
{
|
||||
category: 'cloud',
|
||||
field: 'cloud.instance.id',
|
||||
values: ['5896613765949631815'],
|
||||
originalValue: '5896613765949631815',
|
||||
},
|
||||
{ category: 'cloud', field: 'cloud.provider', values: ['gcp'], originalValue: 'gcp' },
|
||||
{
|
||||
category: 'cloud',
|
||||
field: 'cloud.machine.type',
|
||||
values: ['e2-medium'],
|
||||
originalValue: 'e2-medium',
|
||||
},
|
||||
{
|
||||
category: 'cloud',
|
||||
field: 'cloud.project.id',
|
||||
values: ['elastic-siem'],
|
||||
originalValue: 'elastic-siem',
|
||||
},
|
||||
{
|
||||
category: 'base',
|
||||
field: '@timestamp',
|
||||
values: ['2020-11-25T15:42:39.417Z'],
|
||||
originalValue: '2020-11-25T15:42:39.417Z',
|
||||
},
|
||||
{
|
||||
category: 'related',
|
||||
field: 'related.user',
|
||||
values: ['administrator'],
|
||||
originalValue: 'administrator',
|
||||
},
|
||||
{ category: 'ecs', field: 'ecs.version', values: ['1.5.0'], originalValue: '1.5.0' },
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.hostname',
|
||||
values: ['windows-native'],
|
||||
originalValue: 'windows-native',
|
||||
},
|
||||
{ category: 'host', field: 'host.os.build', values: ['17763.1577'], originalValue: '17763.1577' },
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.os.kernel',
|
||||
values: ['10.0.17763.1577 (WinBuild.160101.0800)'],
|
||||
originalValue: '10.0.17763.1577 (WinBuild.160101.0800)',
|
||||
},
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.os.name',
|
||||
values: ['Windows Server 2019 Datacenter'],
|
||||
originalValue: 'Windows Server 2019 Datacenter',
|
||||
},
|
||||
{ category: 'host', field: 'host.os.family', values: ['windows'], originalValue: 'windows' },
|
||||
{ category: 'host', field: 'host.os.version', values: ['10.0'], originalValue: '10.0' },
|
||||
{ category: 'host', field: 'host.os.platform', values: ['windows'], originalValue: 'windows' },
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.ip',
|
||||
values: ['fe80::406c:d205:5b46:767f', '10.128.15.228'],
|
||||
originalValue: ['fe80::406c:d205:5b46:767f', '10.128.15.228'],
|
||||
},
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.name',
|
||||
values: ['windows-native'],
|
||||
originalValue: 'windows-native',
|
||||
},
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.id',
|
||||
values: ['08f50e68-847a-4fae-a8eb-c7dc886447bb'],
|
||||
originalValue: '08f50e68-847a-4fae-a8eb-c7dc886447bb',
|
||||
},
|
||||
{
|
||||
category: 'host',
|
||||
field: 'host.mac',
|
||||
values: ['42:01:0a:80:0f:e4'],
|
||||
originalValue: ['42:01:0a:80:0f:e4'],
|
||||
},
|
||||
{ category: 'host', field: 'host.architecture', values: ['x86_64'], originalValue: 'x86_64' },
|
||||
{
|
||||
category: 'event',
|
||||
field: 'event.ingested',
|
||||
values: ['2020-11-25T15:36:40.924914552Z'],
|
||||
originalValue: '2020-11-25T15:36:40.924914552Z',
|
||||
},
|
||||
{ category: 'event', field: 'event.code', values: [4625], originalValue: 4625 },
|
||||
{ category: 'event', field: 'event.lag.total', values: [2077], originalValue: 2077 },
|
||||
{ category: 'event', field: 'event.lag.read', values: [1075], originalValue: 1075 },
|
||||
{ category: 'event', field: 'event.lag.ingest', values: [1002], originalValue: 1002 },
|
||||
{
|
||||
category: 'event',
|
||||
field: 'event.provider',
|
||||
values: ['Microsoft-Windows-Security-Auditing'],
|
||||
originalValue: 'Microsoft-Windows-Security-Auditing',
|
||||
},
|
||||
{
|
||||
category: 'event',
|
||||
field: 'event.created',
|
||||
values: ['2020-11-25T15:36:39.922Z'],
|
||||
originalValue: '2020-11-25T15:36:39.922Z',
|
||||
},
|
||||
{ category: 'event', field: 'event.kind', values: ['signal'], originalValue: 'signal' },
|
||||
{ category: 'event', field: 'event.module', values: ['security'], originalValue: 'security' },
|
||||
{
|
||||
category: 'event',
|
||||
field: 'event.action',
|
||||
values: ['logon-failed'],
|
||||
originalValue: 'logon-failed',
|
||||
},
|
||||
{ category: 'event', field: 'event.type', values: ['start'], originalValue: 'start' },
|
||||
{
|
||||
category: 'event',
|
||||
field: 'event.category',
|
||||
values: ['authentication'],
|
||||
originalValue: 'authentication',
|
||||
},
|
||||
{ category: 'event', field: 'event.outcome', values: ['failure'], originalValue: 'failure' },
|
||||
{
|
||||
category: 'user',
|
||||
field: 'user.name',
|
||||
values: ['administrator'],
|
||||
originalValue: 'administrator',
|
||||
},
|
||||
{ category: 'user', field: 'user.id', values: ['S-1-0-0'], originalValue: 'S-1-0-0' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.parents',
|
||||
values: [
|
||||
'{"id":"688MAHYB7WTwW_Glsi_d","type":"event","index":"winlogbeat-7.10.0-2020.11.12-000001","depth":0}',
|
||||
],
|
||||
originalValue: [
|
||||
{
|
||||
id: '688MAHYB7WTwW_Glsi_d',
|
||||
type: 'event',
|
||||
index: 'winlogbeat-7.10.0-2020.11.12-000001',
|
||||
depth: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.ancestors',
|
||||
values: [
|
||||
'{"id":"688MAHYB7WTwW_Glsi_d","type":"event","index":"winlogbeat-7.10.0-2020.11.12-000001","depth":0}',
|
||||
],
|
||||
originalValue: [
|
||||
{
|
||||
id: '688MAHYB7WTwW_Glsi_d',
|
||||
type: 'event',
|
||||
index: 'winlogbeat-7.10.0-2020.11.12-000001',
|
||||
depth: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ category: 'signal', field: 'signal.status', values: ['open'], originalValue: 'open' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.id',
|
||||
values: ['b69d086c-325a-4f46-b17b-fb6d227006ba'],
|
||||
originalValue: 'b69d086c-325a-4f46-b17b-fb6d227006ba',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.rule_id',
|
||||
values: ['e7cd9a53-ac62-44b5-bdec-9c94d85bb1a5'],
|
||||
originalValue: 'e7cd9a53-ac62-44b5-bdec-9c94d85bb1a5',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.actions', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.author', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.false_positives', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.meta.from', values: ['1m'], originalValue: '1m' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.meta.kibana_siem_app_url',
|
||||
values: ['http://localhost:5601/app/security'],
|
||||
originalValue: 'http://localhost:5601/app/security',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.max_signals', values: [100], originalValue: 100 },
|
||||
{ category: 'signal', field: 'signal.rule.risk_score', values: [21], originalValue: 21 },
|
||||
{ category: 'signal', field: 'signal.rule.risk_score_mapping', values: [], originalValue: [] },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.output_index',
|
||||
values: ['.siem-signals-angelachuang-default'],
|
||||
originalValue: '.siem-signals-angelachuang-default',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.description', values: ['xxx'], originalValue: 'xxx' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.from',
|
||||
values: ['now-360s'],
|
||||
originalValue: 'now-360s',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.index',
|
||||
values: [
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
originalValue: [
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.interval', values: ['5m'], originalValue: '5m' },
|
||||
{ category: 'signal', field: 'signal.rule.language', values: ['kuery'], originalValue: 'kuery' },
|
||||
{ category: 'signal', field: 'signal.rule.license', values: [''], originalValue: '' },
|
||||
{ category: 'signal', field: 'signal.rule.name', values: ['xxx'], originalValue: 'xxx' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.query',
|
||||
values: ['@timestamp : * '],
|
||||
originalValue: '@timestamp : * ',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.references', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.severity', values: ['low'], originalValue: 'low' },
|
||||
{ category: 'signal', field: 'signal.rule.severity_mapping', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.tags', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.type', values: ['query'], originalValue: 'query' },
|
||||
{ category: 'signal', field: 'signal.rule.to', values: ['now'], originalValue: 'now' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.filters',
|
||||
values: [
|
||||
'{"meta":{"alias":null,"negate":false,"disabled":false,"type":"exists","key":"message","value":"exists"},"exists":{"field":"message"},"$state":{"store":"appState"}}',
|
||||
],
|
||||
originalValue: [
|
||||
{
|
||||
meta: {
|
||||
alias: null,
|
||||
negate: false,
|
||||
disabled: false,
|
||||
type: 'exists',
|
||||
key: 'message',
|
||||
value: 'exists',
|
||||
},
|
||||
exists: { field: 'message' },
|
||||
$state: { store: 'appState' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.created_by',
|
||||
values: ['angela'],
|
||||
originalValue: 'angela',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.updated_by',
|
||||
values: ['angela'],
|
||||
originalValue: 'angela',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.threat', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.rule.version', values: [2], originalValue: 2 },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.created_at',
|
||||
values: ['2020-11-24T10:30:33.660Z'],
|
||||
originalValue: '2020-11-24T10:30:33.660Z',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.rule.updated_at',
|
||||
values: ['2020-11-25T15:37:40.939Z'],
|
||||
originalValue: '2020-11-25T15:37:40.939Z',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.rule.exceptions_list', values: [], originalValue: [] },
|
||||
{ category: 'signal', field: 'signal.depth', values: [1], originalValue: 1 },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.parent.id',
|
||||
values: ['688MAHYB7WTwW_Glsi_d'],
|
||||
originalValue: '688MAHYB7WTwW_Glsi_d',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.parent.type', values: ['event'], originalValue: 'event' },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.parent.index',
|
||||
values: ['winlogbeat-7.10.0-2020.11.12-000001'],
|
||||
originalValue: 'winlogbeat-7.10.0-2020.11.12-000001',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.parent.depth', values: [0], originalValue: 0 },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_time',
|
||||
values: ['2020-11-25T15:36:38.847Z'],
|
||||
originalValue: '2020-11-25T15:36:38.847Z',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.ingested',
|
||||
values: ['2020-11-25T15:36:40.924914552Z'],
|
||||
originalValue: '2020-11-25T15:36:40.924914552Z',
|
||||
},
|
||||
{ category: 'signal', field: 'signal.original_event.code', values: [4625], originalValue: 4625 },
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.lag.total',
|
||||
values: [2077],
|
||||
originalValue: 2077,
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.lag.read',
|
||||
values: [1075],
|
||||
originalValue: 1075,
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.lag.ingest',
|
||||
values: [1002],
|
||||
originalValue: 1002,
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.provider',
|
||||
values: ['Microsoft-Windows-Security-Auditing'],
|
||||
originalValue: 'Microsoft-Windows-Security-Auditing',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.created',
|
||||
values: ['2020-11-25T15:36:39.922Z'],
|
||||
originalValue: '2020-11-25T15:36:39.922Z',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.kind',
|
||||
values: ['event'],
|
||||
originalValue: 'event',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.module',
|
||||
values: ['security'],
|
||||
originalValue: 'security',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.action',
|
||||
values: ['logon-failed'],
|
||||
originalValue: 'logon-failed',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.type',
|
||||
values: ['start'],
|
||||
originalValue: 'start',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.category',
|
||||
values: ['authentication'],
|
||||
originalValue: 'authentication',
|
||||
},
|
||||
{
|
||||
category: 'signal',
|
||||
field: 'signal.original_event.outcome',
|
||||
values: ['failure'],
|
||||
originalValue: 'failure',
|
||||
},
|
||||
{
|
||||
category: '_index',
|
||||
field: '_index',
|
||||
values: ['.siem-signals-angelachuang-default-000004'],
|
||||
originalValue: '.siem-signals-angelachuang-default-000004',
|
||||
},
|
||||
{
|
||||
category: '_id',
|
||||
field: '_id',
|
||||
values: ['5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'],
|
||||
originalValue: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31',
|
||||
},
|
||||
{ category: '_score', field: '_score', values: [1], originalValue: 1 },
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.agent.name',
|
||||
values: ['windows-native'],
|
||||
originalValue: ['windows-native'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.cloud.machine.type',
|
||||
values: ['e2-medium'],
|
||||
originalValue: ['e2-medium'],
|
||||
},
|
||||
{ category: 'fields', field: 'fields.cloud.provider', values: ['gcp'], originalValue: ['gcp'] },
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.agent.id',
|
||||
values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'],
|
||||
originalValue: ['abfe4a35-d5b4-42a0-a539-bd054c791769'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.cloud.instance.id',
|
||||
values: ['5896613765949631815'],
|
||||
originalValue: ['5896613765949631815'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.agent.type',
|
||||
values: ['winlogbeat'],
|
||||
originalValue: ['winlogbeat'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.@timestamp',
|
||||
values: ['2020-11-25T15:42:39.417Z'],
|
||||
originalValue: ['2020-11-25T15:42:39.417Z'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.agent.ephemeral_id',
|
||||
values: ['b9850845-c000-4ddd-bd51-9978a07b7e7d'],
|
||||
originalValue: ['b9850845-c000-4ddd-bd51-9978a07b7e7d'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.cloud.instance.name',
|
||||
values: ['windows-native'],
|
||||
originalValue: ['windows-native'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.cloud.availability_zone',
|
||||
values: ['us-central1-a'],
|
||||
originalValue: ['us-central1-a'],
|
||||
},
|
||||
{
|
||||
category: 'fields',
|
||||
field: 'fields.agent.version',
|
||||
values: ['7.10.0'],
|
||||
originalValue: ['7.10.0'],
|
||||
},
|
||||
];
|
|
@ -566,6 +566,13 @@ exports[`EventDetails rendering should match snapshot 1`] = `
|
|||
"902",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.kind",
|
||||
"originalValue": "event",
|
||||
"values": Array [
|
||||
"event",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
eventId="Y-6TfmcB0WOhS6qyMv3s"
|
||||
|
@ -1139,6 +1146,13 @@ exports[`EventDetails rendering should match snapshot 1`] = `
|
|||
"902",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.kind",
|
||||
"originalValue": "event",
|
||||
"values": Array [
|
||||
"event",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
eventId="Y-6TfmcB0WOhS6qyMv3s"
|
||||
|
@ -1296,6 +1310,13 @@ exports[`EventDetails rendering should match snapshot 1`] = `
|
|||
"902",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"field": "event.kind",
|
||||
"originalValue": "event",
|
||||
"values": Array [
|
||||
"event",
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -43,6 +43,9 @@ exports[`JSON View rendering should match snapshot 1`] = `
|
|||
\\"ip\\": \\"10.47.8.200\\",
|
||||
\\"packets\\": 4,
|
||||
\\"port\\": 902
|
||||
},
|
||||
\\"event\\": {
|
||||
\\"kind\\": \\"event\\"
|
||||
}
|
||||
}"
|
||||
width="100%"
|
||||
|
|
|
@ -58,6 +58,7 @@ export const getColumns = ({
|
|||
onUpdateColumns,
|
||||
contextId,
|
||||
toggleColumn,
|
||||
getLinkValue,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
columnHeaders: ColumnHeaderOptions[];
|
||||
|
@ -65,6 +66,7 @@ export const getColumns = ({
|
|||
onUpdateColumns: OnUpdateColumns;
|
||||
contextId: string;
|
||||
toggleColumn: (column: ColumnHeaderOptions) => void;
|
||||
getLinkValue: (field: string) => string | null;
|
||||
}) => [
|
||||
{
|
||||
field: 'field',
|
||||
|
@ -187,6 +189,7 @@ export const getColumns = ({
|
|||
fieldName={data.field}
|
||||
fieldType={data.type}
|
||||
value={value}
|
||||
linkValue={getLinkValue(data.field)}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -9,37 +9,45 @@ import React from 'react';
|
|||
|
||||
import '../../mock/match_media';
|
||||
import '../../mock/react_beautiful_dnd';
|
||||
import {
|
||||
defaultHeaders,
|
||||
mockDetailItemData,
|
||||
mockDetailItemDataId,
|
||||
TestProviders,
|
||||
} from '../../mock';
|
||||
import { mockDetailItemData, mockDetailItemDataId, TestProviders } from '../../mock';
|
||||
|
||||
import { EventDetails, View } from './event_details';
|
||||
import { EventDetails, EventsViewType } from './event_details';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
import { mockAlertDetailsData } from './__mocks__';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
|
||||
jest.mock('../link_to');
|
||||
describe('EventDetails', () => {
|
||||
const mount = useMountAppended();
|
||||
const defaultProps = {
|
||||
browserFields: mockBrowserFields,
|
||||
columnHeaders: defaultHeaders,
|
||||
data: mockDetailItemData,
|
||||
id: mockDetailItemDataId,
|
||||
view: 'table-view' as View,
|
||||
onUpdateColumns: jest.fn(),
|
||||
isAlert: false,
|
||||
onViewSelected: jest.fn(),
|
||||
timelineId: 'test',
|
||||
toggleColumn: jest.fn(),
|
||||
view: EventsViewType.summaryView,
|
||||
};
|
||||
|
||||
const alertsProps = {
|
||||
...defaultProps,
|
||||
data: mockAlertDetailsData as TimelineEventsDetailsItem[],
|
||||
isAlert: true,
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<EventDetails {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const alertsWrapper = mount(
|
||||
<TestProviders>
|
||||
<EventDetails {...alertsProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
describe('rendering', () => {
|
||||
test('should match snapshot', () => {
|
||||
const shallowWrap = shallow(<EventDetails {...defaultProps} />);
|
||||
|
@ -65,4 +73,27 @@ describe('EventDetails', () => {
|
|||
).toEqual('Table');
|
||||
});
|
||||
});
|
||||
|
||||
describe('alerts tabs', () => {
|
||||
['Summary', 'Table', 'JSON View'].forEach((tab) => {
|
||||
test(`it renders the ${tab} tab`, () => {
|
||||
expect(
|
||||
alertsWrapper
|
||||
.find('[data-test-subj="eventDetails"]')
|
||||
.find('[role="tablist"]')
|
||||
.containsMatchingElement(<span>{tab}</span>)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('the Summary tab is selected by default', () => {
|
||||
expect(
|
||||
alertsWrapper
|
||||
.find('[data-test-subj="eventDetails"]')
|
||||
.find('.euiTab-isSelected')
|
||||
.first()
|
||||
.text()
|
||||
).toEqual('Summary');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
|
||||
import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
|
@ -13,17 +13,20 @@ import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/ti
|
|||
import { EventFieldsBrowser } from './event_fields_browser';
|
||||
import { JsonView } from './json_view';
|
||||
import * as i18n from './translations';
|
||||
import { SummaryView } from './summary_view';
|
||||
|
||||
export type View = EventsViewType.tableView | EventsViewType.jsonView;
|
||||
export type View = EventsViewType.tableView | EventsViewType.jsonView | EventsViewType.summaryView;
|
||||
export enum EventsViewType {
|
||||
tableView = 'table-view',
|
||||
jsonView = 'json-view',
|
||||
summaryView = 'summary-view',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineEventsDetailsItem[];
|
||||
id: string;
|
||||
isAlert: boolean;
|
||||
view: EventsViewType;
|
||||
onViewSelected: (selected: EventsViewType) => void;
|
||||
timelineId: string;
|
||||
|
@ -50,13 +53,33 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
view,
|
||||
onViewSelected,
|
||||
timelineId,
|
||||
isAlert,
|
||||
}) => {
|
||||
const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [
|
||||
onViewSelected,
|
||||
]);
|
||||
const handleTabClick = useCallback((e) => onViewSelected(e.id), [onViewSelected]);
|
||||
|
||||
const alerts = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: EventsViewType.summaryView,
|
||||
name: i18n.SUMMARY,
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<SummaryView
|
||||
data={data}
|
||||
eventId={id}
|
||||
browserFields={browserFields}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[data, id, browserFields, timelineId]
|
||||
);
|
||||
const tabs: EuiTabbedContentTab[] = useMemo(
|
||||
() => [
|
||||
...(isAlert ? alerts : []),
|
||||
{
|
||||
id: EventsViewType.tableView,
|
||||
name: i18n.TABLE,
|
||||
|
@ -83,10 +106,10 @@ const EventDetailsComponent: React.FC<Props> = ({
|
|||
),
|
||||
},
|
||||
],
|
||||
[browserFields, data, id, timelineId]
|
||||
[alerts, browserFields, data, id, isAlert, timelineId]
|
||||
);
|
||||
|
||||
const selectedTab = view === EventsViewType.tableView ? tabs[0] : tabs[1];
|
||||
const selectedTab = useMemo(() => tabs.find((t) => t.id === view) ?? tabs[0], [tabs, view]);
|
||||
|
||||
return (
|
||||
<StyledEuiTabbedContent
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { sortBy } from 'lodash';
|
||||
import { getOr, sortBy } from 'lodash/fp';
|
||||
import { EuiInMemoryTable } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -76,7 +76,7 @@ export const EventFieldsBrowser = React.memo<Props>(
|
|||
const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
|
||||
const items = useMemo(
|
||||
() =>
|
||||
sortBy(data, ['field']).map((item) => ({
|
||||
sortBy(['field'], data).map((item) => ({
|
||||
...item,
|
||||
...fieldsByName[item.field],
|
||||
valuesConcatenated: item.values != null ? item.values.join() : '',
|
||||
|
@ -90,6 +90,19 @@ export const EventFieldsBrowser = React.memo<Props>(
|
|||
return getColumnHeaders(columns, browserFields);
|
||||
});
|
||||
|
||||
const getLinkValue = useCallback(
|
||||
(field: string) => {
|
||||
const linkField = (columnHeaders.find((col) => col.id === field) ?? {}).linkField;
|
||||
if (!linkField) {
|
||||
return null;
|
||||
}
|
||||
const linkFieldData = (data ?? []).find((d) => d.field === linkField);
|
||||
const linkFieldValue = getOr(null, 'originalValue', linkFieldData);
|
||||
return linkFieldValue;
|
||||
},
|
||||
[data, columnHeaders]
|
||||
);
|
||||
|
||||
const toggleColumn = useCallback(
|
||||
(column: ColumnHeaderOptions) => {
|
||||
if (columnHeaders.some((c) => c.id === column.id)) {
|
||||
|
@ -126,8 +139,17 @@ export const EventFieldsBrowser = React.memo<Props>(
|
|||
onUpdateColumns,
|
||||
contextId: timelineId,
|
||||
toggleColumn,
|
||||
getLinkValue,
|
||||
}),
|
||||
[browserFields, columnHeaders, eventId, onUpdateColumns, timelineId, toggleColumn]
|
||||
[
|
||||
browserFields,
|
||||
columnHeaders,
|
||||
eventId,
|
||||
onUpdateColumns,
|
||||
timelineId,
|
||||
toggleColumn,
|
||||
getLinkValue,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -54,6 +54,9 @@ describe('JSON View', () => {
|
|||
packets: 4,
|
||||
port: 902,
|
||||
},
|
||||
event: {
|
||||
kind: 'event',
|
||||
},
|
||||
};
|
||||
expect(buildJsonView(mockDetailItemData)).toEqual(expectedData);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { waitFor } from '@testing-library/react';
|
||||
|
||||
import { SummaryViewComponent } from './summary_view';
|
||||
import { mockAlertDetailsData } from './__mocks__';
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async';
|
||||
|
||||
import { TestProviders } from '../../mock';
|
||||
import { mockBrowserFields } from '../../containers/source/mock';
|
||||
import { useMountAppended } from '../../utils/use_mount_appended';
|
||||
|
||||
jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => {
|
||||
return {
|
||||
useRuleAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const props = {
|
||||
data: mockAlertDetailsData as TimelineEventsDetailsItem[],
|
||||
browserFields: mockBrowserFields,
|
||||
eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31',
|
||||
timelineId: 'detections-page',
|
||||
};
|
||||
|
||||
describe('SummaryViewComponent', () => {
|
||||
const mount = useMountAppended();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useRuleAsync as jest.Mock).mockReturnValue({
|
||||
rule: {
|
||||
note: 'investigation guide',
|
||||
},
|
||||
});
|
||||
});
|
||||
test('render correct items', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<SummaryViewComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
test('render investigation guide', async () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<SummaryViewComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("render no investigation guide if it doesn't exist", async () => {
|
||||
(useRuleAsync as jest.Mock).mockReturnValue({
|
||||
rule: {
|
||||
note: null,
|
||||
},
|
||||
});
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<SummaryViewComponent {...props} />
|
||||
</TestProviders>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { get, getOr } from 'lodash/fp';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiInMemoryTable,
|
||||
EuiBasicTableColumn,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { TimelineEventsDetailsItem } from '../../../../common/search_strategy';
|
||||
import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field';
|
||||
import * as i18n from './translations';
|
||||
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
|
||||
import {
|
||||
ALERTS_HEADERS_RISK_SCORE,
|
||||
ALERTS_HEADERS_RULE,
|
||||
ALERTS_HEADERS_SEVERITY,
|
||||
} from '../../../detections/components/alerts_table/translations';
|
||||
import {
|
||||
IP_FIELD_TYPE,
|
||||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
} from '../../../timelines/components/timeline/body/renderers/constants';
|
||||
import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip';
|
||||
import { LineClamp } from '../line_clamp';
|
||||
import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async';
|
||||
|
||||
interface SummaryRow {
|
||||
title: string;
|
||||
description: {
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
value: string;
|
||||
fieldType: string;
|
||||
linkValue: string | undefined;
|
||||
};
|
||||
}
|
||||
type Summary = SummaryRow[];
|
||||
|
||||
const fields = [
|
||||
{ id: 'signal.status' },
|
||||
{ id: '@timestamp' },
|
||||
{
|
||||
id: SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
linkField: 'signal.rule.id',
|
||||
label: ALERTS_HEADERS_RULE,
|
||||
},
|
||||
{ id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY },
|
||||
{ id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE },
|
||||
{ id: 'host.name' },
|
||||
{ id: 'user.name' },
|
||||
{ id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
|
||||
{ id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE },
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)`
|
||||
.euiTableHeaderCell {
|
||||
border: none;
|
||||
}
|
||||
.euiTableRowCell {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEuiDescriptionList = styled(EuiDescriptionList)`
|
||||
padding: 24px 4px 4px;
|
||||
`;
|
||||
|
||||
const getTitle = (title: SummaryRow['title']) => (
|
||||
<EuiTitle size="xxs">
|
||||
<h5>{title}</h5>
|
||||
</EuiTitle>
|
||||
);
|
||||
|
||||
getTitle.displayName = 'getTitle';
|
||||
|
||||
const getDescription = ({
|
||||
contextId,
|
||||
eventId,
|
||||
fieldName,
|
||||
value,
|
||||
fieldType = '',
|
||||
linkValue,
|
||||
}: SummaryRow['description']) => (
|
||||
<FormattedFieldValue
|
||||
contextId={`alert-details-value-formatted-field-value-${contextId}-${eventId}-${fieldName}-${value}`}
|
||||
eventId={eventId}
|
||||
fieldName={fieldName}
|
||||
fieldType={fieldType}
|
||||
value={value}
|
||||
linkValue={linkValue}
|
||||
/>
|
||||
);
|
||||
|
||||
const getSummary = ({
|
||||
data,
|
||||
browserFields,
|
||||
timelineId,
|
||||
eventId,
|
||||
}: {
|
||||
data: TimelineEventsDetailsItem[];
|
||||
browserFields: BrowserFields;
|
||||
timelineId: string;
|
||||
eventId: string;
|
||||
}) => {
|
||||
return data != null
|
||||
? fields.reduce<Summary>((acc, item) => {
|
||||
const field = data.find((d) => d.field === item.id);
|
||||
if (!field) {
|
||||
return acc;
|
||||
}
|
||||
const linkValueField =
|
||||
item.linkField != null && data.find((d) => d.field === item.linkField);
|
||||
const linkValue = getOr(null, 'originalValue.0', linkValueField);
|
||||
const value = getOr(null, 'originalValue.0', field);
|
||||
const category = field.category;
|
||||
const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string;
|
||||
const description = {
|
||||
contextId: timelineId,
|
||||
eventId,
|
||||
fieldName: item.id,
|
||||
value,
|
||||
fieldType: item.fieldType ?? fieldType,
|
||||
linkValue: linkValue ?? undefined,
|
||||
};
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
title: item.label ?? item.id,
|
||||
description,
|
||||
},
|
||||
];
|
||||
}, [])
|
||||
: [];
|
||||
};
|
||||
|
||||
const summaryColumns: Array<EuiBasicTableColumn<SummaryRow>> = [
|
||||
{
|
||||
field: 'title',
|
||||
truncateText: false,
|
||||
render: getTitle,
|
||||
width: '120px',
|
||||
name: '',
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
truncateText: false,
|
||||
render: getDescription,
|
||||
name: '',
|
||||
},
|
||||
];
|
||||
|
||||
export const SummaryViewComponent: React.FC<{
|
||||
browserFields: BrowserFields;
|
||||
data: TimelineEventsDetailsItem[];
|
||||
eventId: string;
|
||||
timelineId: string;
|
||||
}> = ({ data, eventId, timelineId, browserFields }) => {
|
||||
const ruleId = useMemo(
|
||||
() =>
|
||||
getOr(
|
||||
null,
|
||||
'originalValue',
|
||||
data.find((d) => d.field === 'signal.rule.id')
|
||||
),
|
||||
[data]
|
||||
);
|
||||
const { rule: maybeRule } = useRuleAsync(ruleId);
|
||||
const summaryList = useMemo(() => getSummary({ browserFields, data, eventId, timelineId }), [
|
||||
browserFields,
|
||||
data,
|
||||
eventId,
|
||||
timelineId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledEuiInMemoryTable
|
||||
data-test-subj="summary-view"
|
||||
items={summaryList}
|
||||
columns={summaryColumns}
|
||||
compressed
|
||||
/>
|
||||
{maybeRule?.note && (
|
||||
<StyledEuiDescriptionList data-test-subj="summary-view-guide" compressed>
|
||||
<EuiDescriptionListTitle>{i18n.INVESTIGATION_GUIDE}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<LineClamp content={maybeRule?.note} />
|
||||
</EuiDescriptionListDescription>
|
||||
</StyledEuiDescriptionList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SummaryView = React.memo(SummaryViewComponent);
|
|
@ -6,6 +6,17 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summary', {
|
||||
defaultMessage: 'Summary',
|
||||
});
|
||||
|
||||
export const INVESTIGATION_GUIDE = i18n.translate(
|
||||
'xpack.securitySolution.alertDetails.summary.investigationGuide',
|
||||
{
|
||||
defaultMessage: 'Investigation guide',
|
||||
}
|
||||
);
|
||||
|
||||
export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', {
|
||||
defaultMessage: 'Table',
|
||||
});
|
||||
|
|
|
@ -4,19 +4,22 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { some } from 'lodash/fp';
|
||||
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
import { BrowserFields, DocValueFields } from '../../containers/source';
|
||||
import {
|
||||
ExpandableEvent,
|
||||
ExpandableEventTitle,
|
||||
} from '../../../timelines/components/timeline/expandable_event';
|
||||
import { useDeepEqualSelector } from '../../hooks/use_selector';
|
||||
import { useTimelineEventsDetails } from '../../../timelines/containers/details';
|
||||
import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline';
|
||||
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
|
||||
|
||||
const StyledEuiFlyout = styled(EuiFlyout)`
|
||||
z-index: ${({ theme }) => theme.eui.euiZLevel7};
|
||||
|
@ -28,27 +31,33 @@ interface EventDetailsFlyoutProps {
|
|||
timelineId: string;
|
||||
}
|
||||
|
||||
const emptyExpandedEvent = {};
|
||||
|
||||
const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
|
||||
browserFields,
|
||||
docValueFields,
|
||||
timelineId,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedEvent = useDeepEqualSelector(
|
||||
(state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? emptyExpandedEvent
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent
|
||||
);
|
||||
|
||||
const handleClearSelection = useCallback(() => {
|
||||
dispatch(
|
||||
timelineActions.toggleExpandedEvent({
|
||||
timelineId,
|
||||
event: emptyExpandedEvent,
|
||||
})
|
||||
);
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId }));
|
||||
}, [dispatch, timelineId]);
|
||||
|
||||
const [loading, detailsData] = useTimelineEventsDetails({
|
||||
docValueFields,
|
||||
indexName: expandedEvent.indexName!,
|
||||
eventId: expandedEvent.eventId!,
|
||||
skip: !expandedEvent.eventId,
|
||||
});
|
||||
|
||||
const isAlert = useMemo(
|
||||
() => some({ category: 'signal', field: 'signal.rule.id' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
if (!expandedEvent.eventId) {
|
||||
return null;
|
||||
}
|
||||
|
@ -56,13 +65,15 @@ const EventDetailsFlyoutComponent: React.FC<EventDetailsFlyoutProps> = ({
|
|||
return (
|
||||
<StyledEuiFlyout size="s" onClose={handleClearSelection}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<ExpandableEventTitle />
|
||||
<ExpandableEventTitle isAlert={isAlert} loading={loading} />
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<ExpandableEvent
|
||||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
detailsData={detailsData}
|
||||
event={expandedEvent}
|
||||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
|
|
|
@ -65,6 +65,7 @@ const eventsViewerDefaultProps = {
|
|||
deletedEventIds: [],
|
||||
docValueFields: [],
|
||||
end: to,
|
||||
expandedEvent: {},
|
||||
filters: [],
|
||||
id: TimelineId.detectionsPage,
|
||||
indexNames: mockIndexNames,
|
||||
|
@ -78,6 +79,7 @@ const eventsViewerDefaultProps = {
|
|||
query: '',
|
||||
language: 'kql',
|
||||
},
|
||||
handleCloseExpandedEvent: jest.fn(),
|
||||
start: from,
|
||||
sort: [
|
||||
{
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import { isEmpty, some } from 'lodash/fp';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { Direction } from '../../../../common/search_strategy';
|
||||
import { BrowserFields, DocValueFields } from '../../containers/source';
|
||||
import { useTimelineEvents } from '../../../timelines/containers';
|
||||
import { timelineActions } from '../../../timelines/store/timeline';
|
||||
import { useKibana } from '../../lib/kibana';
|
||||
import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model';
|
||||
import { HeaderSection } from '../header_section';
|
||||
|
@ -35,7 +37,7 @@ import { inputsModel } from '../../store';
|
|||
import { useManageTimeline } from '../../../timelines/components/manage_timeline';
|
||||
import { ExitFullScreen } from '../exit_full_screen';
|
||||
import { useFullScreen } from '../../containers/use_full_screen';
|
||||
import { TimelineId } from '../../../../common/types/timeline';
|
||||
import { TimelineExpandedEvent, TimelineId } from '../../../../common/types/timeline';
|
||||
import { GraphOverlay } from '../../../timelines/components/graph_overlay';
|
||||
|
||||
export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px
|
||||
|
@ -101,6 +103,7 @@ interface Props {
|
|||
deletedEventIds: Readonly<string[]>;
|
||||
docValueFields: DocValueFields[];
|
||||
end: string;
|
||||
expandedEvent: TimelineExpandedEvent;
|
||||
filters: Filter[];
|
||||
headerFilterGroup?: React.ReactNode;
|
||||
height?: number;
|
||||
|
@ -128,6 +131,7 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
deletedEventIds,
|
||||
docValueFields,
|
||||
end,
|
||||
expandedEvent,
|
||||
filters,
|
||||
headerFilterGroup,
|
||||
id,
|
||||
|
@ -145,6 +149,7 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
utilityBar,
|
||||
graphEventId,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { globalFullScreen, timelineFullScreen } = useFullScreen();
|
||||
const columnsHeader = isEmpty(columns) ? defaultHeaders : columns;
|
||||
const kibana = useKibana();
|
||||
|
@ -226,6 +231,12 @@ const EventsViewerComponent: React.FC<Props> = ({
|
|||
skip: !canQueryTimeline,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) {
|
||||
dispatch(timelineActions.toggleExpandedEvent({ timelineId: id }));
|
||||
}
|
||||
}, [dispatch, events, expandedEvent, id]);
|
||||
|
||||
const totalCountMinusDeleted = useMemo(
|
||||
() => (totalCount > 0 ? totalCount - deletedEventIds.length : 0),
|
||||
[deletedEventIds.length, totalCount]
|
||||
|
|
|
@ -51,6 +51,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
deletedEventIds,
|
||||
deleteEventQuery,
|
||||
end,
|
||||
expandedEvent,
|
||||
excludedRowRendererIds,
|
||||
filters,
|
||||
headerFilterGroup,
|
||||
|
@ -111,6 +112,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
|
|||
dataProviders={dataProviders!}
|
||||
deletedEventIds={deletedEventIds}
|
||||
end={end}
|
||||
expandedEvent={expandedEvent}
|
||||
isLoadingIndexPattern={isLoadingIndexPattern}
|
||||
filters={globalFilters}
|
||||
headerFilterGroup={headerFilterGroup}
|
||||
|
@ -142,27 +144,29 @@ const makeMapStateToProps = () => {
|
|||
const getInputsTimeline = inputsSelectors.getTimelineSelector();
|
||||
const getGlobalQuerySelector = inputsSelectors.globalQuerySelector();
|
||||
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
|
||||
const getEvents = timelineSelectors.getEventsByIdSelector();
|
||||
const getTimeline = timelineSelectors.getTimelineByIdSelector();
|
||||
const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => {
|
||||
const input: inputsModel.InputsRange = getInputsTimeline(state);
|
||||
const events: TimelineModel = getEvents(state, id) ?? defaultModel;
|
||||
const timeline: TimelineModel = getTimeline(state, id) ?? defaultModel;
|
||||
const {
|
||||
columns,
|
||||
dataProviders,
|
||||
deletedEventIds,
|
||||
excludedRowRendererIds,
|
||||
expandedEvent,
|
||||
graphEventId,
|
||||
itemsPerPage,
|
||||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
sort,
|
||||
showCheckboxes,
|
||||
} = events;
|
||||
} = timeline;
|
||||
|
||||
return {
|
||||
columns,
|
||||
dataProviders,
|
||||
deletedEventIds,
|
||||
expandedEvent,
|
||||
excludedRowRendererIds,
|
||||
filters: getGlobalFiltersQuerySelector(state),
|
||||
id,
|
||||
|
@ -175,7 +179,7 @@ const makeMapStateToProps = () => {
|
|||
showCheckboxes,
|
||||
// Used to determine whether the footer should show (since it is hidden if the graph is showing.)
|
||||
// `getTimeline` actually returns `TimelineModel | undefined`
|
||||
graphEventId: (getTimeline(state, id) as TimelineModel | undefined)?.graphEventId,
|
||||
graphEventId,
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiText } from '@elastic/eui';
|
||||
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const LINE_CLAMP = 3;
|
||||
const LINE_CLAMP_HEIGHT = 4.5;
|
||||
|
||||
const StyledLineClamp = styled.div`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${LINE_CLAMP};
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
max-height: ${`${LINE_CLAMP_HEIGHT}em`};
|
||||
height: ${`${LINE_CLAMP_HEIGHT}em`};
|
||||
`;
|
||||
|
||||
const ReadMore = styled(EuiButtonEmpty)`
|
||||
span.euiButtonContent {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) => {
|
||||
const [isOverflow, setIsOverflow] = useState<boolean | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState<boolean | null>(null);
|
||||
const descriptionRef = useRef<HTMLDivElement>(null);
|
||||
const toggleReadMore = useCallback(() => {
|
||||
setIsExpanded((prevState) => !prevState);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (content != null && descriptionRef?.current?.clientHeight != null) {
|
||||
if (
|
||||
(descriptionRef?.current?.scrollHeight ?? 0) > (descriptionRef?.current?.clientHeight ?? 0)
|
||||
) {
|
||||
setIsOverflow(true);
|
||||
}
|
||||
|
||||
if (
|
||||
((content == null || descriptionRef?.current?.scrollHeight) ?? 0) <=
|
||||
(descriptionRef?.current?.clientHeight ?? 0)
|
||||
) {
|
||||
setIsOverflow(false);
|
||||
}
|
||||
}
|
||||
}, [content]);
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isExpanded ? (
|
||||
<p>{content}</p>
|
||||
) : isOverflow == null || isOverflow === true ? (
|
||||
<StyledLineClamp ref={descriptionRef}>{content}</StyledLineClamp>
|
||||
) : (
|
||||
<EuiText>{content}</EuiText>
|
||||
)}
|
||||
{isOverflow && (
|
||||
<ReadMore onClick={toggleReadMore} size="s" data-test-subj="summary-view-readmore">
|
||||
{isExpanded ? i18n.READ_LESS : i18n.READ_MORE}
|
||||
</ReadMore>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const LineClamp = React.memo(LineClampComponent);
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const READ_MORE = i18n.translate('xpack.securitySolution.alertDetails.summary.readMore', {
|
||||
defaultMessage: 'Read More',
|
||||
});
|
||||
|
||||
export const READ_LESS = i18n.translate('xpack.securitySolution.alertDetails.summary.readLess', {
|
||||
defaultMessage: 'Read Less',
|
||||
});
|
|
@ -109,4 +109,9 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [
|
|||
originalValue: 902,
|
||||
values: ['902'],
|
||||
},
|
||||
{
|
||||
field: 'event.kind',
|
||||
originalValue: 'event',
|
||||
values: ['event'],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -11,7 +11,7 @@ import { FormattedFieldValue } from '../../../timelines/components/timeline/body
|
|||
export const SOURCE_IP_FIELD_NAME = 'source.ip';
|
||||
export const DESTINATION_IP_FIELD_NAME = 'destination.ip';
|
||||
|
||||
const IP_FIELD_TYPE = 'ip';
|
||||
export const IP_FIELD_TYPE = 'ip';
|
||||
|
||||
/**
|
||||
* Renders text containing a draggable IP address (e.g. `source.ip`,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import { omit } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { mockBrowserFields } from '../../../common/containers/source/mock';
|
||||
import { TestProviders } from '../../../common/mock';
|
||||
|
@ -204,6 +205,65 @@ describe('field_items', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('it returns the expected signal column settings', async () => {
|
||||
const mockSelectedCategoryId = 'signal';
|
||||
const mockBrowserFieldsWithSignal = {
|
||||
...mockBrowserFields,
|
||||
signal: {
|
||||
fields: {
|
||||
'signal.rule.name': {
|
||||
aggregatable: true,
|
||||
category: 'signal',
|
||||
description: 'rule name',
|
||||
example: '',
|
||||
format: '',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
name: 'signal.rule.name',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const toggleColumn = jest.fn();
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
<Category
|
||||
categoryId={mockSelectedCategoryId}
|
||||
data-test-subj="category"
|
||||
filteredBrowserFields={mockBrowserFieldsWithSignal}
|
||||
fieldItems={getFieldItems({
|
||||
browserFields: mockBrowserFieldsWithSignal,
|
||||
category: mockBrowserFieldsWithSignal[mockSelectedCategoryId],
|
||||
categoryId: mockSelectedCategoryId,
|
||||
columnHeaders,
|
||||
highlight: '',
|
||||
onUpdateColumns: jest.fn(),
|
||||
timelineId,
|
||||
toggleColumn,
|
||||
})}
|
||||
width={FIELDS_PANE_WIDTH}
|
||||
onCategorySelected={jest.fn()}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="field-signal.rule.name-checkbox"]`)
|
||||
.last()
|
||||
.simulate('change', {
|
||||
target: { checked: true },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toggleColumn).toBeCalledWith({
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'signal.rule.name',
|
||||
width: 180,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders the expected icon for a field', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
|
|
|
@ -6,13 +6,6 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const ALL_ACTIONS = i18n.translate(
|
||||
'xpack.securitySolution.open.timeline.allActionsTooltip',
|
||||
{
|
||||
defaultMessage: 'All actions',
|
||||
}
|
||||
);
|
||||
|
||||
export const BATCH_ACTIONS = i18n.translate(
|
||||
'xpack.securitySolution.open.timeline.batchActionsTitle',
|
||||
{
|
||||
|
|
|
@ -19,16 +19,16 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
} from '@elastic/eui';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import React, { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RowRendererId } from '../../../../common/types/timeline';
|
||||
import { State } from '../../../common/store';
|
||||
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions';
|
||||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
import { renderers } from './catalog';
|
||||
import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions';
|
||||
import { RowRenderersBrowser } from './row_renderers_browser';
|
||||
import * as i18n from './translations';
|
||||
|
||||
|
@ -78,16 +78,14 @@ interface StatefulRowRenderersBrowserProps {
|
|||
timelineId: string;
|
||||
}
|
||||
|
||||
const emptyExcludedRowRendererIds: RowRendererId[] = [];
|
||||
|
||||
const StatefulRowRenderersBrowserComponent: React.FC<StatefulRowRenderersBrowserProps> = ({
|
||||
timelineId,
|
||||
}) => {
|
||||
const tableRef = useRef<EuiInMemoryTable<{}>>();
|
||||
const dispatch = useDispatch();
|
||||
const excludedRowRendererIds = useShallowEqualSelector(
|
||||
(state: State) =>
|
||||
state.timeline.timelineById[timelineId]?.excludedRowRendererIds || emptyExcludedRowRendererIds
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const excludedRowRendererIds = useDeepEqualSelector(
|
||||
(state: State) => (getTimeline(state, timelineId) ?? timelineDefaults).excludedRowRendererIds
|
||||
);
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles';
|
||||
import * as i18n from '../translations';
|
||||
|
@ -66,14 +66,16 @@ const ActionsComponent: React.FC<Props> = ({
|
|||
)}
|
||||
<EventsTd key="expand-event">
|
||||
<EventsTdContent textAlign="center" width={DEFAULT_ICON_BUTTON_WIDTH}>
|
||||
<EuiButtonIcon
|
||||
aria-label={expanded ? i18n.COLLAPSE : i18n.EXPAND}
|
||||
data-test-subj="expand-event"
|
||||
disabled={expanded}
|
||||
iconType="arrowRight"
|
||||
id={eventId}
|
||||
onClick={onEventToggled}
|
||||
/>
|
||||
<EuiToolTip data-test-subj="expand-event-tool-tip" content={i18n.EXPAND_EVENT}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.COLLAPSE}
|
||||
data-test-subj="expand-event"
|
||||
disabled={expanded}
|
||||
iconType="arrowRight"
|
||||
id={eventId}
|
||||
onClick={onEventToggled}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EventsTdContent>
|
||||
</EventsTd>
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
|
|||
import { Ecs } from '../../../../../../common/ecs';
|
||||
import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline';
|
||||
import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model';
|
||||
import { timelineSelectors } from '../../../../store/timeline';
|
||||
import { AssociateNote } from '../../../notes/helpers';
|
||||
import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events';
|
||||
import { EventsTrData } from '../../styles';
|
||||
|
@ -85,8 +86,9 @@ export const EventColumnView = React.memo<Props>(
|
|||
timelineId,
|
||||
toggleShowNotes,
|
||||
}) => {
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const { timelineType, status } = useDeepEqualSelector((state) =>
|
||||
pick(['timelineType', 'status'], state.timeline.timelineById[timelineId])
|
||||
pick(['timelineType', 'status'], getTimeline(state, timelineId))
|
||||
);
|
||||
|
||||
const handlePinClicked = useCallback(
|
||||
|
|
|
@ -26,8 +26,9 @@ import { NoteCards } from '../../../notes/note_cards';
|
|||
import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context';
|
||||
import { EventColumnView } from './event_column_view';
|
||||
import { inputsModel } from '../../../../../common/store';
|
||||
import { timelineActions } from '../../../../store/timeline';
|
||||
import { timelineActions, timelineSelectors } from '../../../../store/timeline';
|
||||
import { activeTimeline } from '../../../../containers/active_timeline_context';
|
||||
import { timelineDefaults } from '../../../../store/timeline/defaults';
|
||||
|
||||
interface Props {
|
||||
actionsColumnWidth: number;
|
||||
|
@ -77,8 +78,9 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({});
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedEvent = useDeepEqualSelector(
|
||||
(state) => state.timeline.timelineById[timelineId].expandedEvent
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent
|
||||
);
|
||||
const divElement = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
@ -112,13 +114,12 @@ const StatefulEventComponent: React.FC<Props> = ({
|
|||
event: {
|
||||
eventId,
|
||||
indexName,
|
||||
loading: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (timelineId === TimelineId.active) {
|
||||
activeTimeline.toggleExpandedEvent({ eventId, indexName, loading: false });
|
||||
activeTimeline.toggleExpandedEvent({ eventId, indexName });
|
||||
}
|
||||
}, [dispatch, event._id, event._index, timelineId]);
|
||||
|
||||
|
|
|
@ -14,3 +14,4 @@ export const RULE_REFERENCE_FIELD_NAME = 'rule.reference';
|
|||
export const REFERENCE_URL_FIELD_NAME = 'reference.url';
|
||||
export const EVENT_URL_FIELD_NAME = 'event.url';
|
||||
export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name';
|
||||
export const SIGNAL_STATUS_FIELD_NAME = 'signal.status';
|
||||
|
|
|
@ -5,19 +5,15 @@
|
|||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { isNumber, isString, isEmpty } from 'lodash/fp';
|
||||
import { isNumber, isEmpty } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
import { Bytes, BYTES_FORMAT } from './bytes';
|
||||
import { Duration, EVENT_DURATION_FIELD_NAME } from '../../../duration';
|
||||
import {
|
||||
getOrEmptyTagFromValue,
|
||||
getEmptyTagValue,
|
||||
} from '../../../../../common/components/empty_value';
|
||||
import { getOrEmptyTagFromValue } from '../../../../../common/components/empty_value';
|
||||
import { FormattedDate } from '../../../../../common/components/formatted_date';
|
||||
import { FormattedIp } from '../../../../components/formatted_ip';
|
||||
import { HostDetailsLink } from '../../../../../common/components/links';
|
||||
|
||||
import { Port, PORT_NAMES } from '../../../../../network/components/port';
|
||||
import { TruncatableText } from '../../../../../common/components/truncatable_text';
|
||||
|
@ -31,9 +27,12 @@ import {
|
|||
SIGNAL_RULE_NAME_FIELD_NAME,
|
||||
REFERENCE_URL_FIELD_NAME,
|
||||
EVENT_URL_FIELD_NAME,
|
||||
SIGNAL_STATUS_FIELD_NAME,
|
||||
GEO_FIELD_TYPE,
|
||||
} from './constants';
|
||||
import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers';
|
||||
import { RuleStatus } from './rule_status';
|
||||
import { HostName } from './host_name';
|
||||
|
||||
// simple black-list to prevent dragging and dropping fields such as message name
|
||||
const columnNamesNotDraggable = [MESSAGE_FIELD_NAME];
|
||||
|
@ -80,22 +79,7 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
<Duration contextId={contextId} eventId={eventId} fieldName={fieldName} value={`${value}`} />
|
||||
);
|
||||
} else if (fieldName === HOST_NAME_FIELD_NAME) {
|
||||
const hostname = `${value}`;
|
||||
|
||||
return isString(value) && hostname.length > 0 ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
|
||||
tooltipContent={value}
|
||||
value={value}
|
||||
>
|
||||
<HostDetailsLink data-test-subj="host-details-link" hostName={hostname}>
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">{value}</TruncatableText>
|
||||
</HostDetailsLink>
|
||||
</DefaultDraggable>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
return <HostName contextId={contextId} eventId={eventId} fieldName={fieldName} value={value} />;
|
||||
} else if (fieldFormat === BYTES_FORMAT) {
|
||||
return (
|
||||
<Bytes contextId={contextId} eventId={eventId} fieldName={fieldName} value={`${value}`} />
|
||||
|
@ -113,6 +97,10 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
);
|
||||
} else if (fieldName === EVENT_MODULE_FIELD_NAME) {
|
||||
return renderEventModule({ contextId, eventId, fieldName, linkValue, truncate, value });
|
||||
} else if (fieldName === SIGNAL_STATUS_FIELD_NAME) {
|
||||
return (
|
||||
<RuleStatus contextId={contextId} eventId={eventId} fieldName={fieldName} value={value} />
|
||||
);
|
||||
} else if (
|
||||
[RULE_REFERENCE_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME].includes(fieldName)
|
||||
) {
|
||||
|
@ -142,7 +130,6 @@ const FormattedFieldValueComponent: React.FC<{
|
|||
} else {
|
||||
const contentValue = getOrEmptyTagFromValue(value);
|
||||
const content = truncate ? <TruncatableText>{contentValue}</TruncatableText> : contentValue;
|
||||
|
||||
return (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
|
|
|
@ -77,6 +77,15 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
{content}
|
||||
</LinkAnchor>
|
||||
</DefaultDraggable>
|
||||
) : value != null ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}-${ruleId}`}
|
||||
tooltipContent={value}
|
||||
value={`${value}`}
|
||||
>
|
||||
{value}
|
||||
</DefaultDraggable>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { isString } from 'lodash/fp';
|
||||
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
import { getEmptyTagValue } from '../../../../../common/components/empty_value';
|
||||
import { HostDetailsLink } from '../../../../../common/components/links';
|
||||
import { TruncatableText } from '../../../../../common/components/truncatable_text';
|
||||
|
||||
interface Props {
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
value: string | number | undefined | null;
|
||||
}
|
||||
|
||||
const HostNameComponent: React.FC<Props> = ({ fieldName, contextId, eventId, value }) => {
|
||||
const hostname = `${value}`;
|
||||
|
||||
return isString(value) && hostname.length > 0 ? (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`event-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
|
||||
tooltipContent={value}
|
||||
value={value}
|
||||
>
|
||||
<HostDetailsLink data-test-subj="host-details-link" hostName={hostname}>
|
||||
<TruncatableText data-test-subj="draggable-truncatable-content">{value}</TruncatableText>
|
||||
</HostDetailsLink>
|
||||
</DefaultDraggable>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
};
|
||||
|
||||
export const HostName = React.memo(HostNameComponent);
|
||||
HostName.displayName = 'HostName';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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, { useMemo } from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { DefaultDraggable } from '../../../../../common/components/draggables';
|
||||
|
||||
const mapping = {
|
||||
open: 'primary',
|
||||
'in-progress': 'warning',
|
||||
closed: 'default',
|
||||
};
|
||||
|
||||
const StyledEuiBadge = styled(EuiBadge)`
|
||||
text-transform: capitalize;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
contextId: string;
|
||||
eventId: string;
|
||||
fieldName: string;
|
||||
value: string | number | undefined | null;
|
||||
}
|
||||
|
||||
const RuleStatusComponent: React.FC<Props> = ({ contextId, eventId, fieldName, value }) => {
|
||||
const color = useMemo(() => getOr('default', `${value}`, mapping), [value]);
|
||||
return (
|
||||
<DefaultDraggable
|
||||
field={fieldName}
|
||||
id={`alert-details-value-default-draggable-${contextId}-${eventId}-${fieldName}-${value}`}
|
||||
value={`${value}`}
|
||||
tooltipContent={fieldName}
|
||||
>
|
||||
<StyledEuiBadge color={color}>{value}</StyledEuiBadge>
|
||||
</DefaultDraggable>
|
||||
);
|
||||
};
|
||||
|
||||
export const RuleStatus = React.memo(RuleStatusComponent);
|
||||
RuleStatus.displayName = 'RuleStatus';
|
|
@ -73,6 +73,13 @@ export const EXPAND = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const EXPAND_EVENT = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.actions.expandEventTooltip',
|
||||
{
|
||||
defaultMessage: 'Expand event',
|
||||
}
|
||||
);
|
||||
|
||||
export const COLLAPSE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.actions.collapseAriaLabel',
|
||||
{
|
||||
|
@ -80,13 +87,6 @@ export const COLLAPSE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const COLLAPSE_EVENT = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.actions.collapseEventTooltip',
|
||||
{
|
||||
defaultMessage: 'Collapse event',
|
||||
}
|
||||
);
|
||||
|
||||
export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate(
|
||||
'xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip',
|
||||
{
|
||||
|
|
|
@ -10,40 +10,66 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { some } from 'lodash/fp';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { BrowserFields, DocValueFields } from '../../../common/containers/source';
|
||||
import {
|
||||
ExpandableEvent,
|
||||
ExpandableEventTitle,
|
||||
HandleOnEventClosed,
|
||||
} from '../../../timelines/components/timeline/expandable_event';
|
||||
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { useTimelineEventsDetails } from '../../containers/details';
|
||||
import { timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
|
||||
interface EventDetailsProps {
|
||||
browserFields: BrowserFields;
|
||||
docValueFields: DocValueFields[];
|
||||
timelineId: string;
|
||||
handleOnEventClosed?: HandleOnEventClosed;
|
||||
}
|
||||
|
||||
const EventDetailsComponent: React.FC<EventDetailsProps> = ({
|
||||
browserFields,
|
||||
docValueFields,
|
||||
timelineId,
|
||||
handleOnEventClosed,
|
||||
}) => {
|
||||
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
|
||||
const expandedEvent = useDeepEqualSelector(
|
||||
(state) => state.timeline.timelineById[timelineId]?.expandedEvent
|
||||
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent
|
||||
);
|
||||
|
||||
const [loading, detailsData] = useTimelineEventsDetails({
|
||||
docValueFields,
|
||||
indexName: expandedEvent.indexName!,
|
||||
eventId: expandedEvent.eventId!,
|
||||
skip: !expandedEvent.eventId,
|
||||
});
|
||||
|
||||
const isAlert = useMemo(
|
||||
() => some({ category: 'signal', field: 'signal.rule.id' }, detailsData),
|
||||
[detailsData]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExpandableEventTitle />
|
||||
<ExpandableEventTitle
|
||||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<ExpandableEvent
|
||||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
detailsData={detailsData}
|
||||
event={expandedEvent}
|
||||
isAlert={isAlert}
|
||||
loading={loading}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</>
|
||||
|
@ -55,5 +81,6 @@ export const EventDetails = React.memo(
|
|||
(prevProps, nextProps) =>
|
||||
deepEqual(prevProps.browserFields, nextProps.browserFields) &&
|
||||
deepEqual(prevProps.docValueFields, nextProps.docValueFields) &&
|
||||
prevProps.timelineId === nextProps.timelineId
|
||||
prevProps.timelineId === nextProps.timelineId &&
|
||||
prevProps.handleOnEventClosed === nextProps.handleOnEventClosed
|
||||
);
|
||||
|
|
|
@ -5,45 +5,74 @@
|
|||
*/
|
||||
|
||||
import { find } from 'lodash/fp';
|
||||
import { EuiTextColor, EuiLoadingContent, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiTextColor,
|
||||
EuiLoadingContent,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { TimelineExpandedEvent } from '../../../../../common/types/timeline';
|
||||
import { BrowserFields, DocValueFields } from '../../../../common/containers/source';
|
||||
import { BrowserFields } from '../../../../common/containers/source';
|
||||
import {
|
||||
EventDetails,
|
||||
EventsViewType,
|
||||
View,
|
||||
} from '../../../../common/components/event_details/event_details';
|
||||
import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline';
|
||||
import { useTimelineEventsDetails } from '../../../containers/details';
|
||||
import { LineClamp } from '../../../../common/components/line_clamp';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export type HandleOnEventClosed = () => void;
|
||||
interface Props {
|
||||
browserFields: BrowserFields;
|
||||
docValueFields: DocValueFields[];
|
||||
detailsData: TimelineEventsDetailsItem[] | null;
|
||||
event: TimelineExpandedEvent;
|
||||
isAlert: boolean;
|
||||
loading: boolean;
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
export const ExpandableEventTitle = React.memo(() => (
|
||||
<EuiTitle size="s">
|
||||
<h4>{i18n.EVENT_DETAILS}</h4>
|
||||
</EuiTitle>
|
||||
));
|
||||
interface ExpandableEventTitleProps {
|
||||
isAlert: boolean;
|
||||
loading: boolean;
|
||||
handleOnEventClosed?: HandleOnEventClosed;
|
||||
}
|
||||
|
||||
const StyledEuiFlexGroup = styled(EuiFlexGroup)`
|
||||
flex: 0;
|
||||
`;
|
||||
|
||||
export const ExpandableEventTitle = React.memo<ExpandableEventTitleProps>(
|
||||
({ isAlert, loading, handleOnEventClosed }) => (
|
||||
<StyledEuiFlexGroup justifyContent="spaceBetween" wrap={true}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="s">
|
||||
{!loading ? <h4>{isAlert ? i18n.ALERT_DETAILS : i18n.EVENT_DETAILS}</h4> : <></>}
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{handleOnEventClosed && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon iconType="cross" aria-label={i18n.CLOSE} onClick={handleOnEventClosed} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</StyledEuiFlexGroup>
|
||||
)
|
||||
);
|
||||
|
||||
ExpandableEventTitle.displayName = 'ExpandableEventTitle';
|
||||
|
||||
export const ExpandableEvent = React.memo<Props>(
|
||||
({ browserFields, docValueFields, event, timelineId }) => {
|
||||
const [view, setView] = useState<View>(EventsViewType.tableView);
|
||||
|
||||
const [loading, detailsData] = useTimelineEventsDetails({
|
||||
docValueFields,
|
||||
indexName: event.indexName!,
|
||||
eventId: event.eventId!,
|
||||
skip: !event.eventId,
|
||||
});
|
||||
({ browserFields, event, timelineId, isAlert, loading, detailsData }) => {
|
||||
const [view, setView] = useState<View>(EventsViewType.summaryView);
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (detailsData) {
|
||||
|
@ -68,12 +97,22 @@ export const ExpandableEvent = React.memo<Props>(
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiText>{message}</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
{message && (
|
||||
<>
|
||||
<EuiDescriptionList data-test-subj="event-message" compressed>
|
||||
<EuiDescriptionListTitle>{i18n.MESSAGE}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
<LineClamp content={message} />
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
<EventDetails
|
||||
browserFields={browserFields}
|
||||
data={detailsData!}
|
||||
id={event.eventId!}
|
||||
isAlert={isAlert}
|
||||
onViewSelected={setView}
|
||||
timelineId={timelineId}
|
||||
view={view}
|
||||
|
|
|
@ -6,6 +6,13 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.expandableEvent.messageTitle',
|
||||
{
|
||||
defaultMessage: 'Message',
|
||||
}
|
||||
);
|
||||
|
||||
export const COPY_TO_CLIPBOARD = i18n.translate(
|
||||
'xpack.securitySolution.timeline.expandableEvent.copyToClipboardToolTip',
|
||||
{
|
||||
|
@ -13,6 +20,13 @@ export const COPY_TO_CLIPBOARD = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CLOSE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.expandableEvent.closeEventDetailsLabel',
|
||||
{
|
||||
defaultMessage: 'close',
|
||||
}
|
||||
);
|
||||
|
||||
export const EVENT = i18n.translate(
|
||||
'xpack.securitySolution.timeline.expandableEvent.eventToolTipTitle',
|
||||
{
|
||||
|
@ -28,8 +42,15 @@ export const EVENT_DETAILS_PLACEHOLDER = i18n.translate(
|
|||
);
|
||||
|
||||
export const EVENT_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.timeline.expandableEvent.titleLabel',
|
||||
'xpack.securitySolution.timeline.expandableEvent.eventTitleLabel',
|
||||
{
|
||||
defaultMessage: 'Event details',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALERT_DETAILS = i18n.translate(
|
||||
'xpack.securitySolution.timeline.expandableEvent.alertTitleLabel',
|
||||
{
|
||||
defaultMessage: 'Alert details',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -261,6 +261,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
}
|
||||
end="2018-03-24T03:33:52.253Z"
|
||||
eventType="all"
|
||||
expandedEvent={Object {}}
|
||||
filters={Array []}
|
||||
isLive={false}
|
||||
itemsPerPage={5}
|
||||
|
@ -273,6 +274,7 @@ In other use cases the message field can be used to concatenate different values
|
|||
}
|
||||
kqlMode="search"
|
||||
kqlQueryExpression=""
|
||||
onEventClosed={[MockFunction]}
|
||||
showCallOutUnauthorizedMsg={false}
|
||||
showEventDetails={false}
|
||||
sort={
|
||||
|
|
|
@ -94,6 +94,7 @@ describe('Timeline', () => {
|
|||
columns: defaultHeaders,
|
||||
dataProviders: mockDataProviders,
|
||||
end: endDate,
|
||||
expandedEvent: {},
|
||||
eventType: 'all',
|
||||
showEventDetails: false,
|
||||
filters: [],
|
||||
|
@ -103,6 +104,7 @@ describe('Timeline', () => {
|
|||
itemsPerPageOptions: [5, 10, 20],
|
||||
kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'],
|
||||
kqlQueryExpression: '',
|
||||
onEventClosed: jest.fn(),
|
||||
showCallOutUnauthorizedMsg: false,
|
||||
sort,
|
||||
start: startDate,
|
||||
|
|
|
@ -13,8 +13,8 @@ import {
|
|||
EuiFlyoutFooter,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { isEmpty, some } from 'lodash/fp';
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Dispatch } from 'redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
@ -32,7 +32,7 @@ import { combineQueries } from '../helpers';
|
|||
import { TimelineRefetch } from '../refetch_timeline';
|
||||
import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public';
|
||||
import { useManageTimeline } from '../../manage_timeline';
|
||||
import { TimelineEventsType } from '../../../../../common/types/timeline';
|
||||
import { TimelineEventsType, TimelineId } from '../../../../../common/types/timeline';
|
||||
import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config';
|
||||
import { SuperDatePicker } from '../../../../common/components/super_date_picker';
|
||||
import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context';
|
||||
|
@ -45,6 +45,8 @@ import { useSourcererScope } from '../../../../common/containers/sourcerer';
|
|||
import { TimelineModel } from '../../../../timelines/store/timeline/model';
|
||||
import { EventDetails } from '../event_details';
|
||||
import { TimelineDatePickerLock } from '../date_picker_lock';
|
||||
import { activeTimeline } from '../../../containers/active_timeline_context';
|
||||
import { ToggleExpandedEvent } from '../../../store/timeline/actions';
|
||||
|
||||
const TimelineHeaderContainer = styled.div`
|
||||
margin-top: 6px;
|
||||
|
@ -141,6 +143,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
dataProviders,
|
||||
end,
|
||||
eventType,
|
||||
expandedEvent,
|
||||
filters,
|
||||
timelineId,
|
||||
isLive,
|
||||
|
@ -148,6 +151,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
itemsPerPageOptions,
|
||||
kqlMode,
|
||||
kqlQueryExpression,
|
||||
onEventClosed,
|
||||
showCallOutUnauthorizedMsg,
|
||||
showEventDetails,
|
||||
start,
|
||||
|
@ -156,18 +160,6 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind,
|
||||
updateEventTypeAndIndexesName,
|
||||
}) => {
|
||||
const [showEventDetailsColumn, setShowEventDetailsColumn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// it should changed only once to true and then stay visible till the component umount
|
||||
setShowEventDetailsColumn((current) => {
|
||||
if (showEventDetails && !current) {
|
||||
return true;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}, [showEventDetails]);
|
||||
|
||||
const {
|
||||
browserFields,
|
||||
docValueFields,
|
||||
|
@ -247,10 +239,27 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
timerangeKind,
|
||||
});
|
||||
|
||||
const handleOnEventClosed = useCallback(() => {
|
||||
onEventClosed({ timelineId });
|
||||
|
||||
if (timelineId === TimelineId.active) {
|
||||
activeTimeline.toggleExpandedEvent({
|
||||
eventId: expandedEvent.eventId!,
|
||||
indexName: expandedEvent.indexName!,
|
||||
});
|
||||
}
|
||||
}, [timelineId, onEventClosed, expandedEvent.eventId, expandedEvent.indexName]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer });
|
||||
}, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) {
|
||||
handleOnEventClosed();
|
||||
}
|
||||
}, [expandedEvent, handleOnEventClosed, events, combinedQueries]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimelineRefetch
|
||||
|
@ -324,7 +333,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
</EventDetailsWidthProvider>
|
||||
) : null}
|
||||
</ScrollableFlexItem>
|
||||
{showEventDetailsColumn && (
|
||||
{showEventDetails && (
|
||||
<>
|
||||
<VerticalRule />
|
||||
<ScrollableFlexItem grow={1}>
|
||||
|
@ -332,6 +341,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
|
|||
browserFields={browserFields}
|
||||
docValueFields={docValueFields}
|
||||
timelineId={timelineId}
|
||||
handleOnEventClosed={handleOnEventClosed}
|
||||
/>
|
||||
</ScrollableFlexItem>
|
||||
</>
|
||||
|
@ -375,6 +385,7 @@ const makeMapStateToProps = () => {
|
|||
dataProviders,
|
||||
eventType: eventType ?? 'raw',
|
||||
end: input.timerange.to,
|
||||
expandedEvent,
|
||||
filters: timelineFilter,
|
||||
timelineId,
|
||||
isLive: input.policy.kind === 'interval',
|
||||
|
@ -404,6 +415,9 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({
|
|||
})
|
||||
);
|
||||
},
|
||||
onEventClosed: (args: ToggleExpandedEvent) => {
|
||||
dispatch(timelineActions.toggleExpandedEvent(args));
|
||||
},
|
||||
});
|
||||
|
||||
const connector = connect(makeMapStateToProps, mapDispatchToProps);
|
||||
|
@ -420,6 +434,7 @@ const QueryTabContent = connector(
|
|||
prevProps.itemsPerPage === nextProps.itemsPerPage &&
|
||||
prevProps.kqlMode === nextProps.kqlMode &&
|
||||
prevProps.kqlQueryExpression === nextProps.kqlQueryExpression &&
|
||||
prevProps.onEventClosed === nextProps.onEventClosed &&
|
||||
prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg &&
|
||||
prevProps.showEventDetails === nextProps.showEventDetails &&
|
||||
prevProps.status === nextProps.status &&
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { isEmpty, noop } from 'lodash/fp';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
|
@ -72,14 +72,6 @@ export const initSortDefault = [
|
|||
},
|
||||
];
|
||||
|
||||
function usePreviousRequest(value: TimelineEventsAllRequestOptions | null) {
|
||||
const ref = useRef<TimelineEventsAllRequestOptions | null>(value);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export const useTimelineEvents = ({
|
||||
docValueFields,
|
||||
endDate,
|
||||
|
@ -105,7 +97,7 @@ export const useTimelineEvents = ({
|
|||
const [timelineRequest, setTimelineRequest] = useState<TimelineEventsAllRequestOptions | null>(
|
||||
null
|
||||
);
|
||||
const prevTimelineRequest = usePreviousRequest(timelineRequest);
|
||||
const prevTimelineRequest = useRef<TimelineEventsAllRequestOptions | null>(null);
|
||||
|
||||
const clearSignalsState = useCallback(() => {
|
||||
if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) {
|
||||
|
@ -159,6 +151,7 @@ export const useTimelineEvents = ({
|
|||
}
|
||||
let didCancel = false;
|
||||
const asyncSearch = async () => {
|
||||
prevTimelineRequest.current = request;
|
||||
abortCtrl.current = new AbortController();
|
||||
setLoading(true);
|
||||
const searchSubscription$ = data.search
|
||||
|
@ -223,6 +216,7 @@ export const useTimelineEvents = ({
|
|||
|
||||
abortCtrl.current.abort();
|
||||
setLoading(false);
|
||||
prevTimelineRequest.current = activeTimeline.getRequest();
|
||||
refetch.current = asyncSearch.bind(null, activeTimeline.getRequest());
|
||||
setTimelineResponse((prevResp) => {
|
||||
const resp = activeTimeline.getResponse();
|
||||
|
@ -331,9 +325,35 @@ export const useTimelineEvents = ({
|
|||
id !== TimelineId.active ||
|
||||
timerangeKind === 'absolute' ||
|
||||
!deepEqual(prevTimelineRequest, timelineRequest)
|
||||
)
|
||||
) {
|
||||
timelineSearch(timelineRequest);
|
||||
}
|
||||
}, [id, prevTimelineRequest, timelineRequest, timelineSearch, timerangeKind]);
|
||||
|
||||
/*
|
||||
cleanup timeline events response when the filters were removed completely
|
||||
to avoid displaying previous query results
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isEmpty(filterQuery)) {
|
||||
setTimelineResponse({
|
||||
id,
|
||||
inspect: {
|
||||
dsl: [],
|
||||
response: [],
|
||||
},
|
||||
refetch: refetchGrid,
|
||||
totalCount: -1,
|
||||
pageInfo: {
|
||||
activePage: 0,
|
||||
querySize: 0,
|
||||
},
|
||||
events: [],
|
||||
loadPage: wrappedLoadPage,
|
||||
updatedAt: 0,
|
||||
});
|
||||
}
|
||||
}, [filterQuery, id, refetchGrid, wrappedLoadPage]);
|
||||
|
||||
return [loading, timelineResponse];
|
||||
};
|
||||
|
|
|
@ -35,9 +35,9 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI
|
|||
'ADD_NOTE_TO_EVENT'
|
||||
);
|
||||
|
||||
interface ToggleExpandedEvent {
|
||||
export interface ToggleExpandedEvent {
|
||||
timelineId: string;
|
||||
event: TimelineExpandedEvent;
|
||||
event?: TimelineExpandedEvent;
|
||||
}
|
||||
export const toggleExpandedEvent = actionCreator<ToggleExpandedEvent>('TOGGLE_EXPANDED_EVENT');
|
||||
|
||||
|
|
|
@ -80,12 +80,14 @@ describe('epicLocalStorage', () => {
|
|||
dataProviders: mockDataProviders,
|
||||
end: endDate,
|
||||
eventType: 'all',
|
||||
expandedEvent: {},
|
||||
filters: [],
|
||||
isLive: false,
|
||||
itemsPerPage: 5,
|
||||
itemsPerPageOptions: [5, 10, 20],
|
||||
kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'],
|
||||
kqlQueryExpression: '',
|
||||
onEventClosed: jest.fn(),
|
||||
showCallOutUnauthorizedMsg: false,
|
||||
showEventDetails: false,
|
||||
start: startDate,
|
||||
|
|
|
@ -177,7 +177,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
|
|||
...state,
|
||||
timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }),
|
||||
}))
|
||||
.case(toggleExpandedEvent, (state, { timelineId, event }) => ({
|
||||
.case(toggleExpandedEvent, (state, { timelineId, event = {} }) => ({
|
||||
...state,
|
||||
timelineById: {
|
||||
...state.timelineById,
|
||||
|
|
|
@ -41,8 +41,6 @@ export const getTimelines = () => timelineByIdSelector;
|
|||
|
||||
export const getTimelineByIdSelector = () => createSelector(selectTimeline, (timeline) => timeline);
|
||||
|
||||
export const getEventsByIdSelector = () => createSelector(selectTimeline, (timeline) => timeline);
|
||||
|
||||
export const getKqlFilterQuerySelector = () =>
|
||||
createSelector(selectTimeline, (timeline) =>
|
||||
timeline &&
|
||||
|
|
|
@ -27,7 +27,7 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory<TimelineEven
|
|||
response: IEsSearchResponse<unknown>
|
||||
): Promise<TimelineEventsDetailsStrategyResponse> => {
|
||||
const { indexName, eventId, docValueFields = [] } = options;
|
||||
const fieldsData = cloneDeep(response.rawResponse.hits.hits[0].fields ?? {});
|
||||
const fieldsData = cloneDeep(response.rawResponse.hits.hits[0]?.fields ?? {});
|
||||
const hitsData = cloneDeep(response.rawResponse.hits.hits[0] ?? {});
|
||||
delete hitsData._source;
|
||||
delete hitsData.fields;
|
||||
|
|
|
@ -17737,7 +17737,6 @@
|
|||
"xpack.securitySolution.notes.notesTitle": "メモ",
|
||||
"xpack.securitySolution.notes.previewMarkdownTitle": "プレビュー(マークダウン)",
|
||||
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター",
|
||||
"xpack.securitySolution.open.timeline.allActionsTooltip": "すべてのアクション",
|
||||
"xpack.securitySolution.open.timeline.batchActionsTitle": "一斉アクション",
|
||||
"xpack.securitySolution.open.timeline.cancelButton": "キャンセル",
|
||||
"xpack.securitySolution.open.timeline.collapseButton": "縮小",
|
||||
|
@ -17945,7 +17944,6 @@
|
|||
"xpack.securitySolution.timeline.autosave.warning.refresh.title": "タイムラインを更新",
|
||||
"xpack.securitySolution.timeline.autosave.warning.title": "更新されるまで自動保存は無効です",
|
||||
"xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "縮小",
|
||||
"xpack.securitySolution.timeline.body.actions.collapseEventTooltip": "イベントを折りたたむ",
|
||||
"xpack.securitySolution.timeline.body.actions.expandAriaLabel": "拡張",
|
||||
"xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "イベントを分析します",
|
||||
"xpack.securitySolution.timeline.body.copyToClipboardButtonLabel": "クリップボードにコピー",
|
||||
|
|
|
@ -17755,7 +17755,6 @@
|
|||
"xpack.securitySolution.notes.notesTitle": "备注",
|
||||
"xpack.securitySolution.notes.previewMarkdownTitle": "预览 (Markdown)",
|
||||
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选",
|
||||
"xpack.securitySolution.open.timeline.allActionsTooltip": "所有操作",
|
||||
"xpack.securitySolution.open.timeline.batchActionsTitle": "批处理操作",
|
||||
"xpack.securitySolution.open.timeline.cancelButton": "取消",
|
||||
"xpack.securitySolution.open.timeline.collapseButton": "折叠",
|
||||
|
@ -17963,7 +17962,6 @@
|
|||
"xpack.securitySolution.timeline.autosave.warning.refresh.title": "刷新时间线",
|
||||
"xpack.securitySolution.timeline.autosave.warning.title": "刷新后才会启用自动保存",
|
||||
"xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "折叠",
|
||||
"xpack.securitySolution.timeline.body.actions.collapseEventTooltip": "折叠事件",
|
||||
"xpack.securitySolution.timeline.body.actions.expandAriaLabel": "展开",
|
||||
"xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "分析事件",
|
||||
"xpack.securitySolution.timeline.body.copyToClipboardButtonLabel": "复制到剪贴板",
|
||||
|
|
Loading…
Reference in a new issue