diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts index 9fa7f96599deb..0346f3bb9439b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts @@ -9,6 +9,7 @@ import { Inspect, Maybe } from '../../../common'; import { TimelineRequestOptionsPaginated } from '../..'; export interface TimelineEventsDetailsItem { + category?: string; field: string; values?: Maybe; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index e447a004fb51c..aa114ff074898 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -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 diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index 1cece57c2fea5..2bfe72033135b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -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); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index e5a673b03449f..0e6226f69fce7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -352,7 +352,6 @@ export const CaseComponent = React.memo( event: { eventId: alertId, indexName: index, - loading: false, }, }) ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts new file mode 100644 index 0000000000000..b4561e6d5bffd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts @@ -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'], + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 8d807825c246a..973d067d9e379 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -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", + ], + }, ] } /> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap index af9fc61b9585c..15f00bbf72cf1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -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%" diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 1a492eee4ae7a..0b2fbcf703d77 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -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)} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index bafe3df1a9cc7..20fa6e54e044d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -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( ); + const alertsWrapper = mount( + + + + ); + describe('rendering', () => { test('should match snapshot', () => { const shallowWrap = shallow(); @@ -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({tab}) + ).toBeTruthy(); + }); + }); + + test('the Summary tab is selected by default', () => { + expect( + alertsWrapper + .find('[data-test-subj="eventDetails"]') + .find('.euiTab-isSelected') + .first() + .text() + ).toEqual('Summary'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 92c3ff9b9fa97..291893fe682b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -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 = ({ 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: ( + <> + + + + ), + }, + ], + [data, id, browserFields, timelineId] + ); const tabs: EuiTabbedContentTab[] = useMemo( () => [ + ...(isAlert ? alerts : []), { id: EventsViewType.tableView, name: i18n.TABLE, @@ -83,10 +106,10 @@ const EventDetailsComponent: React.FC = ({ ), }, ], - [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 ( ( 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( 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( onUpdateColumns, contextId: timelineId, toggleColumn, + getLinkValue, }), - [browserFields, columnHeaders, eventId, onUpdateColumns, timelineId, toggleColumn] + [ + browserFields, + columnHeaders, + eventId, + onUpdateColumns, + timelineId, + toggleColumn, + getLinkValue, + ] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx index 0cf158c8ea90b..da93670d647a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx @@ -54,6 +54,9 @@ describe('JSON View', () => { packets: 4, port: 902, }, + event: { + kind: 'event', + }, }; expect(buildJsonView(mockDetailItemData)).toEqual(expectedData); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx new file mode 100644 index 0000000000000..dec1bd9f3ac69 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx @@ -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( + + + + ); + expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true); + }); + + test('render investigation guide', async () => { + const wrapper = mount( + + + + ); + 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( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx new file mode 100644 index 0000000000000..13d734657acce --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -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']) => ( + +
{title}
+
+); + +getTitle.displayName = 'getTitle'; + +const getDescription = ({ + contextId, + eventId, + fieldName, + value, + fieldType = '', + linkValue, +}: SummaryRow['description']) => ( + +); + +const getSummary = ({ + data, + browserFields, + timelineId, + eventId, +}: { + data: TimelineEventsDetailsItem[]; + browserFields: BrowserFields; + timelineId: string; + eventId: string; +}) => { + return data != null + ? fields.reduce((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> = [ + { + 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 ( + <> + + {maybeRule?.note && ( + + {i18n.INVESTIGATION_GUIDE} + + + + + )} + + ); +}; + +export const SummaryView = React.memo(SummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 19e71e0f37da6..76ae2cd4a88a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -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', }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx index b3a838ab088df..6fecf0d739d1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx @@ -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 = ({ 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 = ({ return ( - + diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 8710503924d84..5e5bdebffa182 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -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: [ { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index c578e017c4d95..69c75bfbea56a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -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; docValueFields: DocValueFields[]; end: string; + expandedEvent: TimelineExpandedEvent; filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; @@ -128,6 +131,7 @@ const EventsViewerComponent: React.FC = ({ deletedEventIds, docValueFields, end, + expandedEvent, filters, headerFilterGroup, id, @@ -145,6 +149,7 @@ const EventsViewerComponent: React.FC = ({ 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 = ({ 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] diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index ec3cbbdef98ad..2570a2b6d1f37 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -51,6 +51,7 @@ const StatefulEventsViewerComponent: React.FC = ({ deletedEventIds, deleteEventQuery, end, + expandedEvent, excludedRowRendererIds, filters, headerFilterGroup, @@ -111,6 +112,7 @@ const StatefulEventsViewerComponent: React.FC = ({ 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; diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx new file mode 100644 index 0000000000000..1b59b174add4a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -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(null); + const [isExpanded, setIsExpanded] = useState(null); + const descriptionRef = useRef(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 ? ( +

{content}

+ ) : isOverflow == null || isOverflow === true ? ( + {content} + ) : ( + {content} + )} + {isOverflow && ( + + {isExpanded ? i18n.READ_LESS : i18n.READ_MORE} + + )} + + ); +}; + +export const LineClamp = React.memo(LineClampComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts b/x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts new file mode 100644 index 0000000000000..e332d1a2d2b5c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts @@ -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', +}); diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts index c5d881c540eec..f074495e65b64 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts @@ -109,4 +109,9 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [ originalValue: 902, values: ['902'], }, + { + field: 'event.kind', + originalValue: 'event', + values: ['event'], + }, ]; diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx index 701094cee88a2..4905fdc2e1f57 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx @@ -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`, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx index f4f8adc9f0419..0e92491be8d18 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx @@ -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( + + + + ); + 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( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 3f391714bb058..268c874de7d50 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -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', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 00cd5453e9669..2ded93377de93 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -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 = ({ timelineId, }) => { const tableRef = useRef>(); 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); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index e942dce724520..b853dc8c81c00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -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 = ({ )} - + + + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 584350f9f7b66..3297d4d613a2b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -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( 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( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 3d3c87be42824..baaf9aa867d90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -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 = ({ }) => { 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(null); @@ -112,13 +114,12 @@ const StatefulEventComponent: React.FC = ({ 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]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index 10518141ebb25..7f3d86af7ca8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 5bd928021fa0b..9c1169608ccae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -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<{ ); } else if (fieldName === HOST_NAME_FIELD_NAME) { - const hostname = `${value}`; - - return isString(value) && hostname.length > 0 ? ( - - - {value} - - - ) : ( - getEmptyTagValue() - ); + return ; } else if (fieldFormat === BYTES_FORMAT) { return ( @@ -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 ( + + ); } 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 ? {contentValue} : contentValue; - return ( = ({ {content} + ) : value != null ? ( + + {value} + ) : ( getEmptyTagValue() ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx new file mode 100644 index 0000000000000..fbac27095d4f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -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 = ({ fieldName, contextId, eventId, value }) => { + const hostname = `${value}`; + + return isString(value) && hostname.length > 0 ? ( + + + {value} + + + ) : ( + getEmptyTagValue() + ); +}; + +export const HostName = React.memo(HostNameComponent); +HostName.displayName = 'HostName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx new file mode 100644 index 0000000000000..4dc6d3b2e8e8d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -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 = ({ contextId, eventId, fieldName, value }) => { + const color = useMemo(() => getOr('default', `${value}`, mapping), [value]); + return ( + + {value} + + ); +}; + +export const RuleStatus = React.memo(RuleStatusComponent); +RuleStatus.displayName = 'RuleStatus'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts index f82f84ec7ad43..c934f50ba0aec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts @@ -73,17 +73,17 @@ export const EXPAND = i18n.translate( } ); -export const COLLAPSE = i18n.translate( - 'xpack.securitySolution.timeline.body.actions.collapseAriaLabel', +export const EXPAND_EVENT = i18n.translate( + 'xpack.securitySolution.timeline.body.actions.expandEventTooltip', { - defaultMessage: 'Collapse', + defaultMessage: 'Expand event', } ); -export const COLLAPSE_EVENT = i18n.translate( - 'xpack.securitySolution.timeline.body.actions.collapseEventTooltip', +export const COLLAPSE = i18n.translate( + 'xpack.securitySolution.timeline.body.actions.collapseAriaLabel', { - defaultMessage: 'Collapse event', + defaultMessage: 'Collapse', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx index ed9b20f7a5e2d..9895f4eda0e6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx @@ -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 = ({ 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 ( <> - + @@ -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 ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index 77a37d8b9a929..5c6bcbccc8e0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -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(() => ( - -

{i18n.EVENT_DETAILS}

-
-)); +interface ExpandableEventTitleProps { + isAlert: boolean; + loading: boolean; + handleOnEventClosed?: HandleOnEventClosed; +} + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + flex: 0; +`; + +export const ExpandableEventTitle = React.memo( + ({ isAlert, loading, handleOnEventClosed }) => ( + + + + {!loading ?

{isAlert ? i18n.ALERT_DETAILS : i18n.EVENT_DETAILS}

: <>} +
+
+ {handleOnEventClosed && ( + + + + )} +
+ ) +); ExpandableEventTitle.displayName = 'ExpandableEventTitle'; export const ExpandableEvent = React.memo( - ({ browserFields, docValueFields, event, timelineId }) => { - const [view, setView] = useState(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(EventsViewType.summaryView); const message = useMemo(() => { if (detailsData) { @@ -68,12 +97,22 @@ export const ExpandableEvent = React.memo( return ( <> - {message} - + {message && ( + <> + + {i18n.MESSAGE} + + + + + + + )} { 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, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 69a7299b9833d..aa3970bba5884 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -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 = ({ dataProviders, end, eventType, + expandedEvent, filters, timelineId, isLive, @@ -148,6 +151,7 @@ export const QueryTabContentComponent: React.FC = ({ itemsPerPageOptions, kqlMode, kqlQueryExpression, + onEventClosed, showCallOutUnauthorizedMsg, showEventDetails, start, @@ -156,18 +160,6 @@ export const QueryTabContentComponent: React.FC = ({ 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 = ({ 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 ( <> = ({ ) : null} - {showEventDetailsColumn && ( + {showEventDetails && ( <> @@ -332,6 +341,7 @@ export const QueryTabContentComponent: React.FC = ({ browserFields={browserFields} docValueFields={docValueFields} timelineId={timelineId} + handleOnEventClosed={handleOnEventClosed} /> @@ -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 && diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 3baab2024558f..9f5aeea695beb 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -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(value); - useEffect(() => { - ref.current = value; - }); - return ref.current; -} - export const useTimelineEvents = ({ docValueFields, endDate, @@ -105,7 +97,7 @@ export const useTimelineEvents = ({ const [timelineRequest, setTimelineRequest] = useState( null ); - const prevTimelineRequest = usePreviousRequest(timelineRequest); + const prevTimelineRequest = useRef(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]; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 479c289cdd21d..b0a0a7e6abe34 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -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('TOGGLE_EXPANDED_EVENT'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 5fcbcf434d3ee..95a916a6858ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index daf57505b6baf..a92a976697eaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -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, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index e379caba323ca..f6386b30d112e 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -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 && diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 0a011d2bfe878..e5b70e22e90b9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -27,7 +27,7 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { 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; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8739c2f09c8fb..7845a003b59ee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -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": "クリップボードにコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3bd6fd13161e9..83c98b794e002 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -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": "复制到剪贴板",