Compare commits

...

243 commits

Author SHA1 Message Date
Steph Milovic 6a687726d5 Small cleanup 2021-11-04 12:51:26 -06:00
Steph Milovic ac0b022e83
[Security Solution] [Sourcerer] Store and type cleanup (#116640) 2021-11-04 12:32:52 -06:00
Steph Milovic 8286a33a40 Merge branch 'main' into sourcerer_kip_as 2021-11-03 14:24:26 -06:00
Steph Milovic 7b13681c24 fix merge 2021-11-01 21:05:19 -06:00
Kibana Machine 2c8adfdee1
Merge branch 'main' into sourcerer_kip_as 2021-11-01 07:31:23 -04:00
Steph Milovic 479014c31b fix mock 2021-10-29 07:01:30 -05:00
Steph Milovic 3704a28847 Show results if sourcerer initiates signal index, whether or not it exists 2021-10-28 15:58:29 -06:00
Steph Milovic e4972f482b fix 2021-10-28 14:10:41 -06:00
Steph Milovic fc5a57e837 this might work 2021-10-28 13:56:49 -06:00
Steph Milovic 2e7c4e6cf0 wip 2021-10-28 10:42:21 -06:00
Steph Milovic 585668d751 Merge branch 'master' into sourcerer_kip_as 2021-10-28 08:25:11 -06:00
Steph Milovic 233299028d Merge branch 'master' into sourcerer_kip_as 2021-10-28 08:16:03 -06:00
Steph Milovic 0db563687d Revert "Merge commit 'refs/pull/116202/merge' of https://github.com/elastic/kibana into sourcerer_kip_as"
This reverts commit be170e1e0c, reversing
changes made to cdcf869fae.
2021-10-27 13:56:57 -06:00
Steph Milovic 748d97a2b7 better comments
https://github.com/elastic/security-team/issues/1978
2021-10-27 13:39:37 -06:00
Steph Milovic 08fba0758a Merge branch 'sourcerer_kip_as' of https://github.com/elastic/kibana into sourcerer_kip_as 2021-10-27 12:56:18 -06:00
Steph Milovic f92432ce41 fix detections roles
detections roles need temporary settings for indexPatterns and savedObjectsManagement. They dont actually need this to pass tests, but we should keep them updated with what the user expects
2021-10-27 12:55:21 -06:00
Kibana Machine 4e14750f8b
Merge branch 'master' into sourcerer_kip_as 2021-10-27 13:17:26 -04:00
Steph Milovic be170e1e0c Merge commit 'refs/pull/116202/merge' of https://github.com/elastic/kibana into sourcerer_kip_as 2021-10-27 10:25:11 -06:00
Paul Tavares 7e8cadbe42
Merge acea7a6d04 into 4be1d8f438 2021-10-27 11:23:50 -04:00
Kibana Machine acea7a6d04
Merge branch 'master' into task/olm-1926-allow-non-superuser-to-read-metadata-api 2021-10-27 11:23:47 -04:00
Steph Milovic cdcf869fae as const message 2021-10-27 08:47:20 -06:00
Steph Milovic 2578fe3f1e Merge branch 'master' into sourcerer_kip_as 2021-10-27 08:13:23 -06:00
Steph Milovic 2e4c7bdeb3 fix type 2021-10-27 08:11:54 -06:00
Kibana Machine 0ef012606d
Merge branch 'master' into task/olm-1926-allow-non-superuser-to-read-metadata-api 2021-10-27 08:17:20 -04:00
Steph Milovic 3ca181de49 rm todos 2021-10-26 19:22:07 -06:00
Steph Milovic 39b6f716ba rm todos 2021-10-26 19:21:07 -06:00
Steph Milovic 0e63da92b0 fix type 2021-10-26 19:18:48 -06:00
Steph Milovic c4c22f71bf resolve conflicts 2021-10-26 18:47:49 -06:00
Paul Tavares 052e07e866 Merge remote-tracking branch 'upstream/master' into task/olm-1926-allow-non-superuser-to-read-metadata-api 2021-10-26 15:35:01 -04:00
Paul Tavares a848216e11 Merge remote-tracking branch 'upstream/master' into task/olm-1926-allow-non-superuser-to-read-metadata-api 2021-10-26 14:22:32 -04:00
Steph Milovic f882d4adcc fix test thanks angela 2021-10-26 10:20:38 -06:00
Steph Milovic 1f4ea7c1c5 doing this 2021-10-26 08:45:30 -06:00
Paul Tavares 90b87a1ebf Merge remote-tracking branch 'upstream/master' into task/olm-1926-allow-non-superuser-to-read-metadata-api 2021-10-26 09:53:14 -04:00
Steph Milovic ca81cd9fe7 Resolve conflicts 2021-10-25 10:49:11 -06:00
Paul Tavares 62a9811f96 Refactor getHostEndpoint() to use new Metadata service as well as the internal kibana ES client 2021-10-25 12:45:40 -04:00
Paul Tavares b6d81103e5 Improve efficiency of getHostEndpoint() search strategy method 2021-10-21 17:18:51 -04:00
Paul Tavares db696c91c2 Add .catch() statement to ES calls in order to get better stacktraces 2021-10-21 17:05:28 -04:00
Steph Milovic e1721d544e merge in masteR 2021-10-21 11:28:00 -06:00
Steph Milovic 6699b29a72 ok dont do the migration fine 2021-10-21 11:18:24 -06:00
Steph Milovic 856cfe7832 fix merge 2021-10-20 09:40:12 -06:00
Steph Milovic fc4c9bf0ee revert silly 2021-10-19 13:03:27 -06:00
Steph Milovic 53bd342494 replace validatePatternListActive 2021-10-19 13:02:04 -06:00
Steph Milovic 27f153c864 Merge branch 'master' into sourcerer_kip_as 2021-10-19 08:41:38 -06:00
Steph Milovic 33caa2143c Merge branch 'master' into sourcerer_kip_as 2021-10-19 07:43:59 -06:00
Steph Milovic becd6bc30c Merge branch 'master' into sourcerer_kip_as 2021-10-19 07:18:05 -06:00
Steph Milovic a7eafca5da review iii 2021-10-19 07:15:43 -06:00
Kibana Machine 2a868e449c
Merge branch 'master' into sourcerer_kip_as 2021-10-18 23:29:19 -04:00
Steph Milovic db06e55ffe simplify 2021-10-18 19:27:48 -06:00
Steph Milovic 770f206e3d Merge branch 'sourcerer_kip_as' of github.com:elastic/kibana into sourcerer_kip_as 2021-10-18 19:22:55 -06:00
Steph Milovic 6a167d87e6 rm validatePatterns 2021-10-18 19:22:37 -06:00
Kibana Machine fc516ea1bc
Merge branch 'master' into sourcerer_kip_as 2021-10-18 18:01:15 -04:00
Steph Milovic 13e6971354 Merge branch 'sourcerer_kip_as' of github.com:elastic/kibana into sourcerer_kip_as 2021-10-18 13:40:22 -06:00
Steph Milovic d592c1b35b hide signal index on default sourcerer 2021-10-18 13:38:56 -06:00
Kibana Machine 9d0fc8f983
Merge branch 'master' into sourcerer_kip_as 2021-10-18 13:09:37 -04:00
Kibana Machine 9cad466130
Merge branch 'master' into sourcerer_kip_as 2021-10-18 10:26:18 -04:00
Steph Milovic 79699a509f Merge branch 'master' into sourcerer_kip_as 2021-10-15 10:45:30 -06:00
Steph Milovic 64e123275c pick events merge conflict 2021-10-15 10:39:39 -06:00
Steph Milovic 5a4f97d0cb fix bug pablo found 2021-10-15 09:59:29 -06:00
Steph Milovic 8441b4ee86 Merge branch 'master' into sourcerer_kip_as 2021-10-14 12:37:40 -06:00
Steph Milovic ffcd6ed664 fix cases privileges test 2021-10-14 12:31:35 -06:00
Steph Milovic f4d8061172 resolve conflicts 2021-10-13 07:34:47 -06:00
Steph Milovic f6635c8246 Fix types 2021-10-11 08:29:57 -06:00
Steph Milovic 7d804a2dd2 Resolve merge 2021-10-11 07:42:46 -06:00
Steph Milovic 6e2a36442b fix routes 2021-10-07 09:36:00 -06:00
Steph Milovic e04ffb0921 Merge branch 'master' into sourcerer_kip_as 2021-10-07 07:21:31 -06:00
Steph Milovic 0f5c93bb47 resolve conflicts 2021-10-06 20:54:14 -06:00
Steph Milovic b7a3cafac9 fix timeline bug 2021-10-06 15:13:00 -06:00
Kibana Machine 8e8d84ca8e
Merge branch 'master' into sourcerer_kip_as 2021-10-06 11:31:43 -04:00
Steph Milovic 732e3dcff3 Merge branch 'master' into sourcerer_kip_as 2021-10-05 18:22:49 -06:00
Steph Milovic 87bd0ab526 fix jest 2021-10-05 18:22:13 -06:00
Steph Milovic 36c4a7b868 rm wildcard from pattern 2021-10-05 13:46:45 -06:00
Steph Milovic 1552c11f68 add trick to cypress 2021-10-05 12:30:54 -06:00
Steph Milovic aebe8e0778 revert some stuff 2021-10-05 12:29:29 -06:00
Steph Milovic f715950e10 Merge branch 'master' into sourcerer_kip_as 2021-10-05 12:21:02 -06:00
Steph Milovic 1d2e8ed71e fix conflict 2021-10-05 08:21:53 -06:00
Steph Milovic 4df9c415b4 revert 2021-10-05 08:19:24 -06:00
Steph Milovic d8cf285227 fix cypress 2021-10-04 20:03:03 -06:00
Steph Milovic 358cad800f review ii 2021-10-04 19:44:17 -06:00
Steph Milovic d535295afc rm test comments 2021-10-04 13:28:41 -06:00
Steph Milovic 3b17d0f2e5 Merge branch 'master' into sourcerer_kip_as 2021-10-04 12:48:49 -06:00
Steph Milovic c3471e66cb add test for johnny 2021-10-04 12:46:13 -06:00
Steph Milovic b202ff2b82 review i 2021-10-04 12:11:58 -06:00
Steph Milovic f6b7b38b31 fix trnslations and other cleanups 2021-10-04 10:30:46 -06:00
Steph Milovic 9c825e92ed rm whoops 2021-10-04 09:30:22 -06:00
Steph Milovic ab6c47e90c merge in master 2021-10-04 09:29:32 -06:00
Steph Milovic 9b01c572c9 resolve tests 2021-10-04 09:26:36 -06:00
Steph Milovic 95516b0a66 stopping point commit 2021-10-01 17:05:19 -06:00
Steph Milovic 4c65fcbdbe merge in master 2021-10-01 07:01:01 -06:00
Steph Milovic 4bdb4910f2 fix jest 2021-10-01 06:49:26 -06:00
Steph Milovic 552b64e4db fix lil whoops again 2021-09-30 17:39:49 -06:00
Steph Milovic 33760fdba4 Fix top n 2021-09-30 17:26:04 -06:00
Steph Milovic fff38a73e2 cleanup comment 2021-09-30 16:51:31 -06:00
Steph Milovic 377108d404 rm useMemo from sourcerer components 2021-09-30 16:47:15 -06:00
Steph Milovic be89ffb572 fix lil whoops 2021-09-30 16:44:23 -06:00
Steph Milovic 71b590a6d4 wildcard to siem signals default index in sourcerer 2021-09-30 16:19:16 -06:00
Steph Milovic 9efc30701a fix conflict 2021-09-29 15:20:53 -06:00
Steph Milovic 96558bcc38 make everything happy 2021-09-29 14:57:52 -06:00
Steph Milovic bed0e0b709 all ready 2021-09-29 12:49:35 -06:00
Steph Milovic 72cae51926 cypress is one crazy mofo 2021-09-29 09:41:52 -06:00
Steph Milovic 82b7327676 merged w master 2021-09-28 15:37:08 -06:00
Steph Milovic e7ae515638 fixes 2021-09-28 15:26:06 -06:00
Steph Milovic 656739e831 resolve conflicts 2021-09-28 11:03:46 -06:00
Steph Milovic 5f96a4ddcf fix bug 2021-09-28 11:01:48 -06:00
Steph Milovic 55020b1d7d push console logs for X 2021-09-27 07:56:18 -06:00
Kibana Machine 26c30ff3c4
Merge branch 'master' into sourcerer_kip_as 2021-09-25 08:59:57 -04:00
Steph Milovic a49893a867 more jest fixing 2021-09-23 14:05:10 -06:00
Steph Milovic 9af3d1e288 fix jest 2021-09-23 13:53:07 -06:00
Steph Milovic 883929aeda Merge branch 'sourcerer_kip_as' of github.com:stephmilovic/kibana into sourcerer_kip_as 2021-09-23 13:17:31 -06:00
Steph Milovic 7c7a968f76 fixes for url state 2021-09-23 13:17:13 -06:00
Kibana Machine 38e8f6ea36
Merge branch 'master' into sourcerer_kip_as 2021-09-23 14:51:35 -04:00
Steph Milovic cc98f56fb2 comments 2021-09-23 11:47:20 -06:00
Steph Milovic 016d18b778 fixed 2021-09-23 11:37:41 -06:00
Steph Milovic 0832cfe841 works with no ruleRegistry 2021-09-23 07:48:16 -06:00
Steph Milovic d1ba923b22 news 2021-09-22 15:36:52 -06:00
Steph Milovic 8730d6366d its A solution. not THE solution 2021-09-22 12:13:57 -06:00
Steph Milovic 9fa9a2fca1 this is not elegant 2021-09-22 11:47:28 -06:00
Steph Milovic 935b5516c9 Merge branch 'sourcerer_kip_as' into sourcerer_kip_cs 2021-09-22 11:06:01 -06:00
Steph Milovic 70c52924ea fix type err 2021-09-22 09:54:00 -06:00
Steph Milovic aa5b258f9b Merge branch 'sourcerer_kip_as' into sourcerer_kip_cs 2021-09-22 09:42:19 -06:00
Steph Milovic 83cc7d2615 fix merge conflicts 2021-09-22 09:39:55 -06:00
Steph Milovic f3c8d41c00 Merge branch 'master' into sourcerer_kip 2021-09-22 09:36:01 -06:00
Steph Milovic de87a9e65a what happened 2021-09-22 09:34:06 -06:00
Steph Milovic 2bc12b23af fix eslint and i18n 2021-09-21 14:29:15 -06:00
Steph Milovic d81fed2fa8 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-21 12:36:53 -06:00
Steph Milovic a79e9ebaf9 Merge branch 'master' into sourcerer_kip 2021-09-21 12:36:37 -06:00
Steph Milovic 3d03457ef3 ruleRegistry:false caps 2021-09-21 12:35:46 -06:00
Steph Milovic dbfa580181 Merge branch 'sourcerer_kip_as' into sourcerer_kip_bs 2021-09-20 15:19:16 -06:00
Steph Milovic ddc4ffe378 more tests wiP 2021-09-20 15:18:56 -06:00
Steph Milovic 2c1aabed8b more test 2021-09-20 14:20:44 -06:00
Steph Milovic a7e31ec297 add more tests 2021-09-20 10:04:32 -06:00
Steph Milovic 84865d31fd fix conflicts 2021-09-20 08:59:18 -06:00
Steph Milovic 12f5ae5964 fix conflicts 2021-09-20 08:55:42 -06:00
Steph Milovic b9b224cee1 resolve 2021-09-20 08:45:53 -06:00
Steph Milovic d9588fd208 jest wip 2021-09-20 08:39:06 -06:00
Steph Milovic 54be286730 working on unit tests 2021-09-16 11:11:05 -06:00
Steph Milovic cdf8171afd merged 2021-09-16 08:04:49 -06:00
Steph Milovic 5936acf0c7 Merge branch 'master' into sourcerer_kip 2021-09-16 07:42:49 -06:00
Steph Milovic 03b7ba2923 bs 2021-09-16 07:40:49 -06:00
Steph Milovic 7e50a47ad7 fix migration and add another test 2021-09-14 22:00:28 -06:00
Steph Milovic f50b40d90b fix whoops 2021-09-14 15:25:15 -06:00
Steph Milovic 1c6c34e450 cy wip
?
2021-09-14 15:18:38 -06:00
Steph Milovic 85fac4b4fb fix 2021-09-14 12:55:01 -06:00
Steph Milovic c9a1905457 merge in 2021-09-14 12:19:13 -06:00
Steph Milovic b9a8d40d3e why did i do this in one commit jeeeeez 2021-09-14 12:16:00 -06:00
Steph Milovic 10b7f2714f Merge branch 'master' into sourcerer_kip 2021-09-14 07:48:59 -06:00
Steph Milovic 438a5c94bb wip 2021-09-13 18:37:57 -06:00
Steph Milovic a8483e042b fix private rtf 2021-09-13 09:24:26 -06:00
Steph Milovic 6f79940d30 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-13 08:55:06 -06:00
Steph Milovic 7e81988cde Merge branch 'master' into sourcerer_kip 2021-09-13 08:54:46 -06:00
Steph Milovic 352d437283 fix index pattern when siem signal index is null 2021-09-10 14:37:20 -06:00
Steph Milovic 382d27c55a Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-10 12:27:53 -06:00
Steph Milovic 0e209ffc99 Merge branch 'master' into sourcerer_kip 2021-09-10 12:27:30 -06:00
Steph Milovic 0903dc12d6 add issue comments 2021-09-10 12:20:56 -06:00
Steph Milovic 302f4a395a yay 2021-09-10 10:34:06 -06:00
Steph Milovic be70eca907 better types 2021-09-10 10:00:29 -06:00
Steph Milovic 6f856a4ce9 updates fields api and enforce types 2021-09-10 09:53:13 -06:00
Steph Milovic 26ef3ee663 more renaming 2021-09-10 06:58:58 -06:00
Steph Milovic a7b851da0c fix translations 2021-09-09 16:03:57 -06:00
Steph Milovic e32f7af58c renames 2021-09-09 15:58:56 -06:00
Steph Milovic 7c86ed6f0e merge in masteR 2021-09-09 14:52:45 -06:00
Steph Milovic c083cdd026 merge master 2021-09-09 14:51:34 -06:00
Steph Milovic b35251c0f3 fix test 2021-09-09 14:42:54 -06:00
Steph Milovic 5ae36e5846 make runtime fields return runtime values, duh 2021-09-09 12:16:00 -06:00
Steph Milovic 38b679cd44 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-08 11:00:29 -06:00
Steph Milovic 03be321931 Merge branch 'master' into sourcerer_kip 2021-09-08 11:00:12 -06:00
Steph Milovic bcc8061b02 jest fixing 2021-09-08 10:08:13 -06:00
Steph Milovic d8c3f204cf jest fixing 2021-09-07 15:41:54 -06:00
Steph Milovic 415ad8d78a revert whoops 2021-09-07 14:13:19 -06:00
Steph Milovic 290a4e1ba5 undo myob 2021-09-07 14:08:05 -06:00
Steph Milovic 08f22aade2 rm logs 2021-09-07 13:53:23 -06:00
Steph Milovic b035d4b0a1 by golly i think i did it 2021-09-07 13:52:39 -06:00
Steph Milovic f52c33b241 added prop to timeline and passes type check 2021-09-07 13:19:55 -06:00
Steph Milovic 8c1b8d0077 server side dataViewId 2021-09-07 11:04:05 -06:00
Steph Milovic 2f5d60a1e1 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-07 09:21:36 -06:00
Steph Milovic bbc7865cf3 Merge branch 'master' into sourcerer_kip 2021-09-07 09:21:10 -06:00
Steph Milovic 26231bb7ac get rid of bad selector 2021-09-07 08:28:57 -06:00
Steph Milovic 3d32cf55b4 About to do timeline stuff 2021-09-07 08:04:50 -06:00
Steph Milovic f80ca48e41 fixing 2021-09-03 12:59:09 -06:00
Steph Milovic a93d4c6934 maeffn type pass wut 2021-09-03 11:37:21 -06:00
Steph Milovic 0705be6e17 fixing tests and types 2021-09-03 11:14:43 -06:00
Steph Milovic 1fe521e1c9 fix some tests and simplify 2021-09-02 18:24:09 -06:00
Steph Milovic 43661fa98a fix bug 2021-09-02 15:00:48 -06:00
Steph Milovic bff7ccc85b start on timeline sourcerer 2021-09-02 12:15:54 -06:00
Steph Milovic 4312ac2304 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-02 11:50:42 -06:00
Steph Milovic ebf945536d Merge branch 'master' into sourcerer_kip 2021-09-02 11:50:29 -06:00
Steph Milovic 67ee52bc3f url state just works wow what a good morning 2021-09-02 10:02:10 -06:00
Steph Milovic 7db7a32c15 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-09-02 08:42:38 -06:00
Steph Milovic bbd8616cc5 Merge branch 'master' into sourcerer_kip_as 2021-09-02 08:03:35 -06:00
Steph Milovic 3492cb06c6 Merge branch 'master' into sourcerer_kip 2021-09-02 08:01:04 -06:00
Steph Milovic ffe53709c3 fixed some things, wrote a test 2021-09-01 16:01:13 -06:00
Steph Milovic eaebc6b096 fix big ol bugsy wugsy 2021-09-01 15:23:27 -06:00
Steph Milovic 5e6331cf52 hey 2021-09-01 13:31:43 -06:00
Steph Milovic 232a5207da fix conflicts 2021-09-01 08:50:15 -06:00
Steph Milovic 31c0b16356 fix merge conflicts 2021-09-01 08:46:34 -06:00
Steph Milovic 74e4c31f87 logs 2021-08-31 14:02:47 -06:00
Steph Milovic 45a1b15984 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-08-31 13:28:48 -06:00
Steph Milovic 09dd0e85e4 Merge branch 'master' into sourcerer_kip 2021-08-31 13:28:36 -06:00
Steph Milovic 68efdd2284 Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-08-31 10:58:41 -06:00
Steph Milovic 7ceef3a346 merge master 2021-08-31 10:58:27 -06:00
Steph Milovic 5748a6cd22 merge w master 2021-08-31 10:57:13 -06:00
Steph Milovic d687623f71 sanity check 2021-08-11 07:52:56 -06:00
Steph Milovic f59180979d working better 2021-08-10 11:21:50 -06:00
Steph Milovic 7ddc9fbe3c Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-08-10 08:18:47 -06:00
Steph Milovic 51625e6ca9 Merge branch 'sourcerer_kip' of github.com:stephmilovic/kibana into sourcerer_kip 2021-08-10 08:13:41 -06:00
Steph Milovic f7760d70df Merge branch 'master' into sourcerer_kip 2021-08-10 08:06:52 -06:00
Steph Milovic 6ea14db6e7 working default need indicesExist 2021-08-09 15:50:47 -06:00
Steph Milovic 2c81a43f4a combo box is back 2021-08-09 13:53:27 -06:00
Steph Milovic 1431aa8e83 updates from advanced settings 2021-08-09 11:36:42 -06:00
Kibana Machine 19a1e9a3e7
Merge branch 'master' into sourcerer_kip 2021-08-09 13:01:14 -04:00
Steph Milovic bc751f0b1a Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-08-09 07:29:38 -06:00
Steph Milovic e986d9da86 fix conflicts 2021-08-09 07:25:58 -06:00
Steph Milovic ff8aa1ca85 idk 2021-08-09 07:22:22 -06:00
Steph Milovic bfeb49f924 rm some logs 2021-08-05 15:15:46 -06:00
Steph Milovic e33abe3cfe Merge branch 'sourcerer_kip' into sourcerer_kip_as 2021-08-05 14:25:03 -06:00
Steph Milovic 32025be2c3 Merge branch 'sourcerer_kip' of github.com:stephmilovic/kibana into sourcerer_kip 2021-08-05 14:24:38 -06:00
Steph Milovic f4341d9284 default sourcerer working me thinks 2021-08-05 14:22:59 -06:00
Steph Milovic 4e81552ad9 wip 2021-08-05 09:47:12 -06:00
Kibana Machine 11c741dcb7
Merge branch 'master' into sourcerer_kip 2021-08-05 09:55:00 -04:00
Steph Milovic 385a91d8ba start 2021-08-05 07:47:21 -06:00
Steph Milovic 23fba2c87d Merge branch 'master' into sourcerer_kip 2021-08-04 08:25:53 -06:00
Steph Milovic ffc08eb0a0 fix ml test 2021-08-04 08:25:06 -06:00
Steph Milovic de97889dc7 merge master 2021-08-03 12:34:59 -06:00
Steph Milovic 49098d46f8 fix types 2021-07-27 15:33:08 -05:00
Steph Milovic 434f9bb7c4 fix mistakes 2021-07-27 11:27:59 -05:00
Steph Milovic 255bc1f0ee pass as arg 2021-07-27 11:21:40 -05:00
Steph Milovic 86bc00dfca ui for permissions and fix tests 2021-07-27 10:51:45 -05:00
Steph Milovic 343354dd54 rm cy com 2021-07-27 06:58:53 -05:00
Steph Milovic 6cc917c527 move to server 2021-07-27 06:53:48 -05:00
Steph Milovic 3c13e86e28 k 2021-07-26 11:20:09 -05:00
Steph Milovic e79a9b13cc merge 2021-07-26 06:39:56 -05:00
Steph Milovic 33d44f1b73 Merge branch 'master' into sourcerer_kip 2021-07-22 20:34:33 -05:00
Steph Milovic d10ec88d10 fix cy 2021-07-22 20:33:10 -05:00
Steph Milovic a386a117fb why 2021-07-22 05:40:32 -06:00
Steph Milovic af76d50d59 Merge branch 'master' into sourcerer_kip 2021-07-22 05:32:26 -06:00
Steph Milovic 0342da683a Merge branch 'master' into sourcerer_kip 2021-07-21 15:16:14 -06:00
Steph Milovic c3df13574e rm some comments 2021-07-21 13:19:10 -06:00
Steph Milovic 8458b6fa48 more test fixes 2021-07-21 13:01:55 -06:00
Steph Milovic 957bb97327 Merge branch 'master' into sourcerer_kip 2021-07-21 10:12:45 -06:00
Steph Milovic 3b6cf6a90e another test fix 2021-07-21 08:53:51 -06:00
Steph Milovic 919a3101ed fix sourcerer jest 2021-07-21 08:33:28 -06:00
Steph Milovic fde9056dc5 no more config patterns 2021-07-20 10:08:38 -06:00
Steph Milovic 8c452c203a rm configIndexPatterns 2021-07-19 12:01:23 -06:00
Steph Milovic 4deabab205 initialize default KIP 2021-07-19 11:27:26 -06:00
258 changed files with 4550 additions and 3185 deletions

View file

@ -1,7 +1,7 @@
[[dashboard-api]]
== Import and export dashboard APIs
deprecated::[7.15.0,These experimental APIs have been deprecated in favor of <<saved-objects-api-import>> and <<saved-objects-api-export>>.]
deprecated::[7.15.0,Both of these APIs have been deprecated in favor of <<saved-objects-api-import>> and <<saved-objects-api-export>>.]
Import and export dashboards with the corresponding saved objects, such as visualizations, saved
searches, and index patterns.

View file

@ -6,7 +6,7 @@
deprecated::[7.15.0,Use <<saved-objects-api-export>> instead.]
experimental[] Export dashboards and corresponding saved objects.
Export dashboards and corresponding saved objects.
[[dashboard-api-export-request]]
==== Request

View file

@ -6,7 +6,7 @@
deprecated::[7.15.0,Use <<saved-objects-api-import>> instead.]
experimental[] Import dashboards and corresponding saved objects.
Import dashboards and corresponding saved objects.
[[dashboard-api-import-request]]
==== Request

View file

@ -8,7 +8,7 @@
import React, { useCallback, useMemo, useState } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { IndexPatternBase, IndexPatternFieldBase } from '@kbn/es-query';
import { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
import {
getGenericComboBoxProps,
@ -20,14 +20,14 @@ const AS_PLAIN_TEXT = { asPlainText: true };
interface OperatorProps {
fieldInputWidth?: number;
fieldTypeFilter?: string[];
indexPattern: IndexPatternBase | undefined;
indexPattern: DataViewBase | undefined;
isClearable: boolean;
isDisabled: boolean;
isLoading: boolean;
isRequired?: boolean;
onChange: (a: IndexPatternFieldBase[]) => void;
onChange: (a: DataViewFieldBase[]) => void;
placeholder: string;
selectedField: IndexPatternFieldBase | undefined;
selectedField: DataViewFieldBase | undefined;
}
export const FieldComponent: React.FC<OperatorProps> = ({
@ -56,7 +56,7 @@ export const FieldComponent: React.FC<OperatorProps> = ({
const handleValuesChange = useCallback(
(newOptions: EuiComboBoxOptionOption[]): void => {
const newValues: IndexPatternFieldBase[] = newOptions.map(
const newValues: DataViewFieldBase[] = newOptions.map(
({ label }) => availableFields[labels.indexOf(label)]
);
onChange(newValues);
@ -94,13 +94,13 @@ export const FieldComponent: React.FC<OperatorProps> = ({
FieldComponent.displayName = 'Field';
interface ComboBoxFields {
availableFields: IndexPatternFieldBase[];
selectedFields: IndexPatternFieldBase[];
availableFields: DataViewFieldBase[];
selectedFields: DataViewFieldBase[];
}
const getComboBoxFields = (
indexPattern: IndexPatternBase | undefined,
selectedField: IndexPatternFieldBase | undefined,
indexPattern: DataViewBase | undefined,
selectedField: DataViewFieldBase | undefined,
fieldTypeFilter: string[]
): ComboBoxFields => {
const existingFields = getExistingFields(indexPattern);
@ -113,29 +113,27 @@ const getComboBoxFields = (
const getComboBoxProps = (fields: ComboBoxFields): GetGenericComboBoxPropsReturn => {
const { availableFields, selectedFields } = fields;
return getGenericComboBoxProps<IndexPatternFieldBase>({
return getGenericComboBoxProps<DataViewFieldBase>({
getLabel: (field) => field.name,
options: availableFields,
selectedOptions: selectedFields,
});
};
const getExistingFields = (indexPattern: IndexPatternBase | undefined): IndexPatternFieldBase[] => {
const getExistingFields = (indexPattern: DataViewBase | undefined): DataViewFieldBase[] => {
return indexPattern != null ? indexPattern.fields : [];
};
const getSelectedFields = (
selectedField: IndexPatternFieldBase | undefined
): IndexPatternFieldBase[] => {
const getSelectedFields = (selectedField: DataViewFieldBase | undefined): DataViewFieldBase[] => {
return selectedField ? [selectedField] : [];
};
const getAvailableFields = (
existingFields: IndexPatternFieldBase[],
selectedFields: IndexPatternFieldBase[],
existingFields: DataViewFieldBase[],
selectedFields: DataViewFieldBase[],
fieldTypeFilter: string[]
): IndexPatternFieldBase[] => {
const fieldsByName = new Map<string, IndexPatternFieldBase>();
): DataViewFieldBase[] => {
const fieldsByName = new Map<string, DataViewFieldBase>();
existingFields.forEach((f) => fieldsByName.set(f.name, f));
selectedFields.forEach((f) => fieldsByName.set(f.name, f));

View file

@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IndexPatternsFetcher } from '.';
import { ElasticsearchClient } from 'kibana/server';
import * as indexNotFoundException from './index_not_found_exception.json';
@ -15,36 +14,36 @@ describe('Index Pattern Fetcher - server', () => {
let esClient: ElasticsearchClient;
const emptyResponse = {
body: {
count: 0,
indices: [],
},
};
const response = {
body: {
count: 1115,
indices: ['b'],
fields: [{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }],
},
};
const patternList = ['a', 'b', 'c'];
beforeEach(() => {
jest.clearAllMocks();
esClient = {
count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response),
fieldCaps: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response),
} as unknown as ElasticsearchClient;
indexPatterns = new IndexPatternsFetcher(esClient);
});
it('Removes pattern without matching indices', async () => {
const result = await indexPatterns.validatePatternListActive(patternList);
expect(result).toEqual(['b', 'c']);
});
it('Returns all patterns when all match indices', async () => {
esClient = {
count: jest.fn().mockResolvedValue(response),
fieldCaps: jest.fn().mockResolvedValue(response),
} as unknown as ElasticsearchClient;
indexPatterns = new IndexPatternsFetcher(esClient);
const result = await indexPatterns.validatePatternListActive(patternList);
expect(result).toEqual(patternList);
});
it('Removes pattern when "index_not_found_exception" error is thrown', async () => {
it('Removes pattern when error is thrown', async () => {
class ServerError extends Error {
public body?: Record<string, any>;
constructor(
@ -56,9 +55,8 @@ describe('Index Pattern Fetcher - server', () => {
this.body = errBody;
}
}
esClient = {
count: jest
fieldCaps: jest
.fn()
.mockResolvedValueOnce(response)
.mockRejectedValue(
@ -69,4 +67,22 @@ describe('Index Pattern Fetcher - server', () => {
const result = await indexPatterns.validatePatternListActive(patternList);
expect(result).toEqual([patternList[0]]);
});
it('When allowNoIndices is false, run validatePatternListActive', async () => {
const fieldCapsMock = jest.fn();
esClient = {
fieldCaps: fieldCapsMock.mockResolvedValue(response),
} as unknown as ElasticsearchClient;
indexPatterns = new IndexPatternsFetcher(esClient);
await indexPatterns.getFieldsForWildcard({ pattern: patternList });
expect(fieldCapsMock.mock.calls).toHaveLength(4);
});
it('When allowNoIndices is true, do not run validatePatternListActive', async () => {
const fieldCapsMock = jest.fn();
esClient = {
fieldCaps: fieldCapsMock.mockResolvedValue(response),
} as unknown as ElasticsearchClient;
indexPatterns = new IndexPatternsFetcher(esClient, true);
await indexPatterns.getFieldsForWildcard({ pattern: patternList });
expect(fieldCapsMock.mock.calls).toHaveLength(1);
});
});

View file

@ -36,12 +36,10 @@ interface FieldSubType {
export class IndexPatternsFetcher {
private elasticsearchClient: ElasticsearchClient;
private allowNoIndices: boolean;
constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) {
this.elasticsearchClient = elasticsearchClient;
this.allowNoIndices = allowNoIndices;
}
/**
* Get a list of field objects for an index pattern that may contain wildcards
*
@ -60,23 +58,22 @@ export class IndexPatternsFetcher {
}): Promise<FieldDescriptor[]> {
const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options;
const patternList = Array.isArray(pattern) ? pattern : pattern.split(',');
const allowNoIndices = fieldCapsOptions
? fieldCapsOptions.allow_no_indices
: this.allowNoIndices;
let patternListActive: string[] = patternList;
// if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless
if (patternList.length > 1) {
if (patternList.length > 1 && !allowNoIndices) {
patternListActive = await this.validatePatternListActive(patternList);
}
const fieldCapsResponse = await getFieldCapabilities(
this.elasticsearchClient,
// if none of the patterns are active, pass the original list to get an error
patternListActive.length > 0 ? patternListActive : patternList,
patternListActive,
metaFields,
{
allow_no_indices: fieldCapsOptions
? fieldCapsOptions.allow_no_indices
: this.allowNoIndices,
allow_no_indices: allowNoIndices,
}
);
if (type === 'rollup' && rollupIndex) {
const rollupFields: FieldDescriptor[] = [];
const rollupIndexCapabilities = getCapabilitiesForRollupIndices(
@ -87,13 +84,11 @@ export class IndexPatternsFetcher {
).body
)[rollupIndex].aggs;
const fieldCapsResponseObj = keyBy(fieldCapsResponse, 'name');
// Keep meta fields
metaFields!.forEach(
(field: string) =>
fieldCapsResponseObj[field] && rollupFields.push(fieldCapsResponseObj[field])
);
return mergeCapabilitiesWithFields(
rollupIndexCapabilities,
fieldCapsResponseObj,
@ -137,23 +132,20 @@ export class IndexPatternsFetcher {
async validatePatternListActive(patternList: string[]) {
const result = await Promise.all(
patternList
.map((pattern) =>
this.elasticsearchClient.count({
index: pattern,
})
)
.map((p) =>
p.catch((e) => {
if (e.body.error.type === 'index_not_found_exception') {
return { body: { count: 0 } };
}
throw e;
})
)
.map(async (index) => {
const searchResponse = await this.elasticsearchClient.fieldCaps({
index,
fields: '_id',
ignore_unavailable: true,
allow_no_indices: false,
});
return searchResponse.body.indices.length > 0;
})
.map((p) => p.catch(() => false))
);
return result.reduce(
(acc: string[], { body: { count } }, patternListIndex) =>
count > 0 ? [...acc, patternList[patternListIndex]] : acc,
(acc: string[], isValid, patternListIndex) =>
isValid ? [...acc, patternList[patternListIndex]] : acc,
[]
);
}

View file

@ -412,6 +412,8 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
},
renderCellValue: getRenderCellValue({ setFlyoutAlert }),
rowRenderers: NO_ROW_RENDER,
// TODO: implement Kibana data view runtime fields in observability
runtimeMappings: {},
start: rangeFrom,
setRefetch,
sort: [

View file

@ -11,8 +11,16 @@ import type { TransformConfigSchema } from './transforms/types';
import { ENABLE_CASE_CONNECTOR } from '../../cases/common';
import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants';
/**
* as const
*
* The const assertion ensures that type widening does not occur
* https://mariusschulz.com/blog/literal-type-widening-in-typescript
* Please follow this convention when adding to this file
*/
export const APP_ID = 'securitySolution' as const;
export const APP_UI_ID = 'securitySolutionUI';
export const APP_UI_ID = 'securitySolutionUI' as const;
export const CASES_FEATURE_ID = 'securitySolutionCases' as const;
export const SERVER_APP_ID = 'siem' as const;
export const APP_NAME = 'Security' as const;
@ -26,6 +34,8 @@ export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const;
export const DEFAULT_DARK_MODE = 'theme:darkMode' as const;
export const DEFAULT_INDEX_KEY = 'securitySolution:defaultIndex' as const;
export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern' as const;
export const DEFAULT_DATA_VIEW_ID = 'security-solution' as const;
export const DEFAULT_TIME_FIELD = '@timestamp' as const;
export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults' as const;
export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults' as const;
export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const;
@ -51,7 +61,6 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges' as const
export const DEFAULT_TRANSFORMS = 'securitySolution:transforms' as const;
export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled' as const;
export const GLOBAL_HEADER_HEIGHT = 96 as const; // px
export const GLOBAL_HEADER_HEIGHT_WITH_GLOBAL_BANNER = 128 as const; // px
export const FILTERS_GLOBAL_HEIGHT = 109 as const; // px
export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled' as const;
export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51' as const;
@ -268,6 +277,7 @@ export const TIMELINE_PREPACKAGED_URL = `${TIMELINE_URL}/_prepackaged` as const;
export const NOTE_URL = '/api/note' as const;
export const PINNED_EVENT_URL = '/api/pinned_event' as const;
export const SOURCERER_API_URL = '/api/sourcerer' as const;
/**
* Default signals index key for kibana.dev.yml
@ -355,7 +365,7 @@ export const ELASTIC_NAME = 'estc' as const;
export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`;
export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_';
export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_' as const;
export const TRANSFORM_STATES = {
ABORTING: 'aborting',

View file

@ -11,7 +11,7 @@ import type {
CreateExceptionListItemSchema,
} from '@kbn/securitysolution-io-ts-list-types';
import { buildExceptionFilter } from '@kbn/securitysolution-list-utils';
import { Filter, EsQueryConfig, IndexPatternBase, buildEsQuery } from '@kbn/es-query';
import { Filter, EsQueryConfig, DataViewBase, buildEsQuery } from '@kbn/es-query';
import { ESBoolQuery } from '../typed_json';
import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas';
@ -24,7 +24,7 @@ export const getQueryFilter = (
lists: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
excludeExceptions: boolean = true
): ESBoolQuery => {
const indexPattern: IndexPatternBase = {
const indexPattern: DataViewBase = {
fields: [],
title: index.join(),
};

View file

@ -5,79 +5,15 @@
* 2.0.
*/
import type { IFieldSubType } from '@kbn/es-query';
import type {
IEsSearchRequest,
IEsSearchResponse,
IIndexPattern,
} from '../../../../../../src/plugins/data/common';
import type { DocValueFields, Maybe } from '../common';
interface FieldInfo {
category: string;
description?: string;
example?: string | number;
format?: string;
name: string;
type?: string;
}
export interface IndexField {
/** Where the field belong */
category: string;
/** Example of field's value */
example?: Maybe<string | number>;
/** whether the field's belong to an alias index */
indexes: Array<Maybe<string>>;
/** The name of the field */
name: string;
/** The type of the field's values as recognized by Kibana */
type: string;
/** Whether the field's values can be efficiently searched for */
searchable: boolean;
/** Whether the field's values can be aggregated */
aggregatable: boolean;
/** Description of the field */
description?: Maybe<string>;
format?: Maybe<string>;
/** the elastic type as mapped in the index */
esTypes?: string[];
subType?: IFieldSubType;
readFromDocValues: boolean;
}
export type BeatFields = Record<string, FieldInfo>;
export interface IndexFieldsStrategyRequest extends IEsSearchRequest {
indices: string[];
onlyCheckIfIndicesExist: boolean;
}
export interface IndexFieldsStrategyResponse extends IEsSearchResponse {
indexFields: IndexField[];
indicesExist: string[];
}
export interface BrowserField {
aggregatable: boolean;
category: string;
description: string | null;
example: string | number | null;
fields: Readonly<Record<string, Partial<BrowserField>>>;
format: string;
indexes: string[];
name: string;
searchable: boolean;
type: string;
subType?: IFieldSubType;
}
export type BrowserFields = Readonly<Record<string, Partial<BrowserField>>>;
export const EMPTY_BROWSER_FIELDS = {};
export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = [];
export const EMPTY_INDEX_PATTERN: IIndexPattern = {
fields: [],
title: '',
};
export {
FieldInfo,
IndexField,
BeatFields,
IndexFieldsStrategyRequest,
IndexFieldsStrategyResponse,
BrowserField,
BrowserFields,
EMPTY_BROWSER_FIELDS,
EMPTY_DOCVALUE_FIELD,
EMPTY_INDEX_FIELDS,
} from '../../../../timelines/common';

View file

@ -10,6 +10,7 @@ export { LastEventIndexKey } from '../../../../../../timelines/common';
export type {
LastTimeDetails,
TimelineEventsLastEventTimeStrategyResponse,
TimelineKpiStrategyRequest,
TimelineKpiStrategyResponse,
TimelineEventsLastEventTimeRequestOptions,
} from '../../../../../../timelines/common';

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { IEsSearchRequest } from '../../../../../../src/plugins/data/common';
import { ESQuery } from '../../typed_json';
import {
@ -41,6 +42,7 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest {
defaultIndex: string[];
docValueFields?: DocValueFields[];
factoryQueryType?: TimelineFactoryQueryTypes;
runtimeMappings: MappingRuntimeFields;
}
export interface TimelineRequestSortField<Field = string> extends SortField<Field> {
@ -171,6 +173,7 @@ export interface SortTimelineInput {
export interface TimelineInput {
columns?: Maybe<ColumnHeaderInput[]>;
dataProviders?: Maybe<DataProviderInput[]>;
dataViewId?: Maybe<string>;
description?: Maybe<string>;
eqlOptions?: Maybe<EqlOptionsInput>;
eventType?: Maybe<string>;

View file

@ -7,12 +7,12 @@
// For the source of these roles please consult the PR these were introduced https://github.com/elastic/kibana/pull/81866#issue-511165754
export enum ROLES {
soc_manager = 'soc_manager',
reader = 'reader',
t1_analyst = 't1_analyst',
t2_analyst = 't2_analyst',
hunter = 'hunter',
rule_author = 'rule_author',
soc_manager = 'soc_manager',
platform_engineer = 'platform_engineer',
detections_admin = 'detections_admin',
}

View file

@ -272,6 +272,7 @@ export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf<typeof TimelineTyp
export const SavedTimelineRuntimeType = runtimeTypes.partial({
columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)),
dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)),
dataViewId: unionWithNullType(runtimeTypes.string),
description: unionWithNullType(runtimeTypes.string),
eqlOptions: unionWithNullType(EqlOptionsRuntimeType),
eventType: unionWithNullType(runtimeTypes.string),
@ -305,7 +306,7 @@ export type SavedTimelineNote = runtimeTypes.TypeOf<typeof SavedTimelineRuntimeT
* This type represents a timeline type stored in a saved object that does not include any fields that reference
* other saved objects.
*/
export type TimelineWithoutExternalRefs = Omit<SavedTimeline, 'savedQueryId'>;
export type TimelineWithoutExternalRefs = Omit<SavedTimeline, 'dataViewId' | 'savedQueryId'>;
/*
* Timeline IDs
@ -719,6 +720,7 @@ export interface TimelineResult {
created?: Maybe<number>;
createdBy?: Maybe<string>;
dataProviders?: Maybe<DataProviderResult[]>;
dataViewId?: Maybe<string>;
dateRange?: Maybe<DateRangePickerResult>;
description?: Maybe<string>;
eqlOptions?: Maybe<EqlOptionsResult>;

View file

@ -38,19 +38,20 @@ export interface SortColumnTimeline {
}
export interface TimelinePersistInput {
id: string;
columns: ColumnHeaderOptions[];
dataProviders?: DataProvider[];
dataViewId: string;
dateRange?: {
start: string;
end: string;
};
defaultColumns?: ColumnHeaderOptions[];
excludedRowRendererIds?: RowRendererId[];
expandedDetail?: TimelineExpandedDetail;
filters?: Filter[];
columns: ColumnHeaderOptions[];
defaultColumns?: ColumnHeaderOptions[];
itemsPerPage?: number;
id: string;
indexNames: string[];
itemsPerPage?: number;
kqlQuery?: {
filterQuery: SerializedFilterQuery | null;
};

View file

@ -12,5 +12,10 @@
"video": false,
"videosFolder": "../../../target/kibana-security-solution/cypress/videos",
"viewportHeight": 900,
"viewportWidth": 1440
"viewportWidth": 1440,
"env": {
"protocol": "http",
"hostname": "localhost",
"configport": "5601"
}
}

View file

@ -0,0 +1 @@
{"savedObjectId":"46cca0e0-2580-11ec-8e56-9dafa0b0343b","version":"WzIyNjIzNCwxXQ==","columns":[{"id":"@timestamp"},{"id":"user.name"},{"id":"event.category"},{"id":"event.action"},{"id":"host.name"}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name: *","kind":"kuery"}}},"dateRange":{"start":"1514809376000","end":"1577881376000"},"description":"This is the best timeline","title":"Security Timeline","created":1633399341550,"createdBy":"elastic","updated":1633399341550,"updatedBy":"elastic","savedQueryId":null,"dataViewId":null,"timelineType":"default","sort":[],"eventNotes":[],"globalNotes":[],"pinnedEventIds":[]}

View file

@ -18,184 +18,28 @@ import {
filterStatusOpen,
} from '../../tasks/create_new_case';
import {
constructUrlWithUser,
getEnvAuth,
loginAndWaitForHostDetailsPage,
loginWithUserAndWaitForPageWithoutDateRange,
logout,
} from '../../tasks/login';
import {
createUsersAndRoles,
deleteUsersAndRoles,
secAll,
secAllUser,
secReadCasesAllUser,
secReadCasesAll,
} from '../../tasks/privileges';
import { CASES_URL } from '../../urls/navigation';
interface User {
username: string;
password: string;
description?: string;
roles: string[];
}
interface UserInfo {
username: string;
full_name: string;
email: string;
}
interface FeaturesPrivileges {
[featureId: string]: string[];
}
interface ElasticsearchIndices {
names: string[];
privileges: string[];
}
interface ElasticSearchPrivilege {
cluster?: string[];
indices?: ElasticsearchIndices[];
}
interface KibanaPrivilege {
spaces: string[];
base?: string[];
feature?: FeaturesPrivileges;
}
interface Role {
name: string;
privileges: {
elasticsearch?: ElasticSearchPrivilege;
kibana?: KibanaPrivilege[];
};
}
const secAll: Role = {
name: 'sec_all_role',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
siem: ['all'],
securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
},
spaces: ['*'],
},
],
},
};
const secAllUser: User = {
username: 'sec_all_user',
password: 'password',
roles: [secAll.name],
};
const secReadCasesAll: Role = {
name: 'sec_read_cases_all_role',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
siem: ['read'],
securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
},
spaces: ['*'],
},
],
},
};
const secReadCasesAllUser: User = {
username: 'sec_read_cases_all_user',
password: 'password',
roles: [secReadCasesAll.name],
};
import { openSourcerer } from '../../tasks/sourcerer';
const usersToCreate = [secAllUser, secReadCasesAllUser];
const rolesToCreate = [secAll, secReadCasesAll];
const getUserInfo = (user: User): UserInfo => ({
username: user.username,
full_name: user.username.replace('_', ' '),
email: `${user.username}@elastic.co`,
});
const createUsersAndRoles = (users: User[], roles: Role[]) => {
const envUser = getEnvAuth();
for (const role of roles) {
cy.log(`Creating role: ${JSON.stringify(role)}`);
cy.request({
body: role.privileges,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'PUT',
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
})
.its('status')
.should('eql', 204);
}
for (const user of users) {
const userInfo = getUserInfo(user);
cy.log(`Creating user: ${JSON.stringify(user)}`);
cy.request({
body: {
username: user.username,
password: user.password,
roles: user.roles,
full_name: userInfo.full_name,
email: userInfo.email,
},
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'POST',
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
})
.its('status')
.should('eql', 200);
}
};
const deleteUsersAndRoles = (users: User[], roles: Role[]) => {
const envUser = getEnvAuth();
for (const user of users) {
cy.log(`Deleting user: ${JSON.stringify(user)}`);
cy.request({
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'DELETE',
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
failOnStatusCode: false,
})
.its('status')
.should('oneOf', [204, 404]);
}
for (const role of roles) {
cy.log(`Deleting role: ${JSON.stringify(role)}`);
cy.request({
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'DELETE',
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
failOnStatusCode: false,
})
.its('status')
.should('oneOf', [204, 404]);
}
// needed to generate index pattern
const visitSecuritySolution = () => {
loginAndWaitForHostDetailsPage();
openSourcerer();
logout();
};
const testCase: TestCaseWithoutTimeline = {
@ -205,11 +49,11 @@ const testCase: TestCaseWithoutTimeline = {
reporter: 'elastic',
owner: 'securitySolution',
};
describe('Cases privileges', () => {
before(() => {
cleanKibana();
createUsersAndRoles(usersToCreate, rolesToCreate);
visitSecuritySolution();
});
after(() => {

View file

@ -5,7 +5,10 @@
* 2.0.
*/
import { loginAndWaitForPage } from '../../tasks/login';
import {
loginAndWaitForPage,
loginWithUserAndWaitForPageWithoutDateRange,
} from '../../tasks/login';
import { HOSTS_URL } from '../../urls/navigation';
import { waitForAllHostsToBeLoaded } from '../../tasks/hosts/all_hosts';
@ -28,20 +31,34 @@ import { openTimelineUsingToggle } from '../../tasks/security_main';
import { populateTimeline } from '../../tasks/timeline';
import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline';
import { cleanKibana } from '../../tasks/common';
import { createUsersAndRoles, secReadCasesAll, secReadCasesAllUser } from '../../tasks/privileges';
import { TOASTER } from '../../screens/configure_cases';
const usersToCreate = [secReadCasesAllUser];
const rolesToCreate = [secReadCasesAll];
// Skipped at the moment as this has flake due to click handler issues. This has been raised with team members
// and the code is being re-worked and then these tests will be unskipped
describe.skip('Sourcerer', () => {
before(() => {
describe('Sourcerer', () => {
beforeEach(() => {
cleanKibana();
});
beforeEach(() => {
cy.clearLocalStorage();
loginAndWaitForPage(HOSTS_URL);
describe('permissions', () => {
before(() => {
createUsersAndRoles(usersToCreate, rolesToCreate);
});
it(`role(s) ${secReadCasesAllUser.roles.join()} shows error when user does not have permissions`, () => {
loginWithUserAndWaitForPageWithoutDateRange(HOSTS_URL, secReadCasesAllUser);
cy.get(TOASTER).should('have.text', 'Write role required to generate data');
});
});
// Originially written in December 2020, flakey from day1
// has always been skipped with intentions to fix, see note at top of file
describe.skip('Default scope', () => {
beforeEach(() => {
cy.clearLocalStorage();
loginAndWaitForPage(HOSTS_URL);
});
describe('Default scope', () => {
it('has SIEM index patterns selected on initial load', () => {
openSourcerer();
isSourcererSelection(`auditbeat-*`);
@ -52,7 +69,7 @@ describe.skip('Sourcerer', () => {
isSourcererOptions([`metrics-*`, `logs-*`]);
});
it('selected KIP gets added to sourcerer', () => {
it('selected DATA_VIEW gets added to sourcerer', () => {
setSourcererOption(`metrics-*`);
openSourcerer();
isSourcererSelection(`metrics-*`);
@ -75,8 +92,14 @@ describe.skip('Sourcerer', () => {
isNotSourcererSelection(`metrics-*`);
});
});
// Originially written in December 2020, flakey from day1
// has always been skipped with intentions to fix
describe.skip('Timeline scope', () => {
beforeEach(() => {
cy.clearLocalStorage();
loginAndWaitForPage(HOSTS_URL);
});
describe('Timeline scope', () => {
const alertPatterns = ['.siem-signals-default'];
const rawPatterns = ['auditbeat-*'];
const allPatterns = [...alertPatterns, ...rawPatterns];

View file

@ -9,6 +9,7 @@ import { ALERT_FLYOUT, CELL_TEXT, JSON_TEXT, TABLE_ROWS } from '../../screens/al
import {
expandFirstAlert,
refreshAlerts,
waitForAlertsIndexToBeCreated,
waitForAlertsPanelToBeLoaded,
} from '../../tasks/alerts';
@ -32,6 +33,7 @@ describe('Alert details with unmapped fields', () => {
createCustomRuleActivated(getUnmappedRule());
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
refreshAlerts();
expandFirstAlert();
});

View file

@ -70,7 +70,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { ALERTS_URL } from '../../urls/navigation';
describe.skip('Detection rules, EQL', () => {
describe('Detection rules, EQL', () => {
const expectedUrls = getEqlRule().referenceUrls.join('');
const expectedFalsePositives = getEqlRule().falsePositivesExamples.join('');
const expectedTags = getEqlRule().tags.join('');
@ -169,7 +169,7 @@ describe.skip('Detection rules, EQL', () => {
});
});
describe.skip('Detection rules, sequence EQL', () => {
describe('Detection rules, sequence EQL', () => {
const expectedNumberOfRules = 1;
const expectedNumberOfSequenceAlerts = '1 alert';

View file

@ -114,7 +114,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { goBackToAllRulesTable } from '../../tasks/rule_details';
import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation';
import { DEFAULT_THREAT_MATCH_QUERY } from '../../../common/constants';
const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d"';
describe('indicator match', () => {
describe('Detection rules, Indicator Match', () => {

View file

@ -34,7 +34,6 @@ import {
waitForRuleToChangeStatus,
} from '../../tasks/alerts_detection_rules';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../../common/constants';
import { ALERTS_URL } from '../../urls/navigation';
import { createCustomRule } from '../../tasks/api_calls/rules';
@ -46,6 +45,8 @@ import {
getNewThresholdRule,
} from '../../objects/rule';
const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000;
describe('Alerts detection rules', () => {
beforeEach(() => {
cleanKibana();

View file

@ -98,7 +98,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery);
cy.url().should(
'include',
'app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -106,7 +106,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery);
cy.url().should(
'include',
'/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -114,7 +114,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery);
cy.url().should(
'include',
'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -122,7 +122,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery);
cy.url().should(
'include',
'/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -130,15 +130,16 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery);
cy.url().should(
'include',
'/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
it('redirects from a $ip$ with a value for the query', () => {
loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery);
cy.url().should(
'include',
'/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
`/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))`
);
});
@ -146,7 +147,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery);
cy.url().should(
'include',
'/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -154,7 +155,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable);
cy.url().should(
'include',
'/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -162,7 +163,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery);
cy.url().should(
'include',
'/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -170,7 +171,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery);
cy.url().should(
'include',
'/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -178,7 +179,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery);
cy.url().should(
'include',
'/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -186,7 +187,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery);
cy.url().should(
'include',
'/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
@ -194,7 +195,7 @@ describe('ml conditional links', () => {
loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery);
cy.url().should(
'include',
'/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))'
'/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))'
);
});
});

View file

@ -121,7 +121,6 @@ describe('Create a timeline from a template', () => {
loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL);
waitForTimelinesPanelToBeLoaded();
});
it('Should have the same query and open the timeline modal', () => {
selectCustomTemplates();
cy.wait('@timeline', { timeout: 100000 });
@ -132,5 +131,6 @@ describe('Create a timeline from a template', () => {
cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible');
cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description);
cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query);
closeTimeline();
});
});

View file

@ -182,11 +182,10 @@ describe('url state', () => {
loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url);
kqlSearch('source.ip: "10.142.0.9" {enter}');
navigateFromHeaderTo(HOSTS);
cy.get(NETWORK).should(
'have.attr',
'href',
`/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))`
`/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))`
);
});
@ -199,12 +198,12 @@ describe('url state', () => {
cy.get(HOSTS).should(
'have.attr',
'href',
`/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
`/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
cy.get(NETWORK).should(
'have.attr',
'href',
`/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
`/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana');
@ -215,21 +214,21 @@ describe('url state', () => {
cy.get(ANOMALIES_TAB).should(
'have.attr',
'href',
"/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')"
"/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')"
);
cy.get(BREADCRUMBS)
.eq(1)
.should(
'have.attr',
'href',
`/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
`/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
cy.get(BREADCRUMBS)
.eq(2)
.should(
'have.attr',
'href',
`/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
`/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))`
);
});

View file

@ -87,6 +87,7 @@ export const expectedExportedTimelineTemplate = (
},
},
},
dataViewId: timelineTemplateBody.dataViewId,
dateRange: {
start: timelineTemplateBody.dateRange?.start,
end: timelineTemplateBody.dateRange?.end,
@ -127,6 +128,7 @@ export const expectedExportedTimeline = (timelineResponse: Cypress.Response<Time
},
},
dateRange: { start: timelineBody.dateRange?.start, end: timelineBody.dateRange?.end },
dataViewId: timelineBody.dataViewId,
description: timelineBody.description,
title: timelineBody.title,
created: timelineBody.created,

View file

@ -7,10 +7,10 @@
export const SOURCERER_TRIGGER = '[data-test-subj="sourcerer-trigger"]';
export const SOURCERER_INPUT =
'[data-test-subj="indexPattern-switcher"] [data-test-subj="comboBoxInput"]';
'[data-test-subj="sourcerer-combo-box"] [data-test-subj="comboBoxInput"]';
export const SOURCERER_OPTIONS =
'[data-test-subj="comboBoxOptionsList indexPattern-switcher-optionsList"]';
export const SOURCERER_SAVE_BUTTON = 'button[data-test-subj="add-index"]';
'[data-test-subj="comboBoxOptionsList sourcerer-combo-box-optionsList"]';
export const SOURCERER_SAVE_BUTTON = 'button[data-test-subj="sourcerer-save"]';
export const SOURCERER_RESET_BUTTON = 'button[data-test-subj="sourcerer-reset"]';
export const SOURCERER_POPOVER_TITLE = '.euiPopoverTitle';
export const HOSTS_STAT = '[data-test-subj="stat-hosts"] [data-test-subj="stat-title"]';

View file

@ -102,6 +102,12 @@ export const goToOpenedAlerts = () => {
cy.get(LOADING_INDICATOR).should('not.exist');
};
export const refreshAlerts = () => {
// ensure we've refetched fields the first time index is defined
cy.get(REFRESH_BUTTON).should('have.text', 'Refresh');
cy.get(REFRESH_BUTTON).first().click({ force: true });
};
export const openFirstAlert = () => {
cy.get(TIMELINE_CONTEXT_MENU_BTN).first().click({ force: true });
cy.get(OPEN_ALERT_BTN).click();

View file

@ -10,7 +10,7 @@ import Url, { UrlObject } from 'url';
import { ROLES } from '../../common/test';
import { TIMELINE_FLYOUT_BODY } from '../screens/timeline';
import { hostDetailsUrl } from '../urls/navigation';
import { hostDetailsUrl, LOGOUT_URL } from '../urls/navigation';
/**
* Credentials in the `kibana.dev.yml` config file will be used to authenticate
@ -326,3 +326,7 @@ export const waitForPageWithoutDateRange = (url: string, role?: ROLES) => {
cy.visit(role ? getUrlWithRoute(role, url) : url);
cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 });
};
export const logout = () => {
cy.visit(LOGOUT_URL);
};

View file

@ -0,0 +1,186 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { constructUrlWithUser, getEnvAuth } from './login';
interface User {
username: string;
password: string;
description?: string;
roles: string[];
}
interface UserInfo {
username: string;
full_name: string;
email: string;
}
interface FeaturesPrivileges {
[featureId: string]: string[];
}
interface ElasticsearchIndices {
names: string[];
privileges: string[];
}
interface ElasticSearchPrivilege {
cluster?: string[];
indices?: ElasticsearchIndices[];
}
interface KibanaPrivilege {
spaces: string[];
base?: string[];
feature?: FeaturesPrivileges;
}
interface Role {
name: string;
privileges: {
elasticsearch?: ElasticSearchPrivilege;
kibana?: KibanaPrivilege[];
};
}
export const secAll: Role = {
name: 'sec_all_role',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
siem: ['all'],
securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
// TODO: Steph/sourcerer remove once we have our internal saved object client
// https://github.com/elastic/security-team/issues/1978
indexPatterns: ['read'],
savedObjectsManagement: ['read'],
},
spaces: ['*'],
},
],
},
};
export const secAllUser: User = {
username: 'sec_all_user',
password: 'password',
roles: [secAll.name],
};
export const secReadCasesAll: Role = {
name: 'sec_read_cases_all_role',
privileges: {
elasticsearch: {
indices: [
{
names: ['*'],
privileges: ['all'],
},
],
},
kibana: [
{
feature: {
siem: ['read'],
securitySolutionCases: ['all'],
actions: ['all'],
actionsSimulators: ['all'],
// TODO: Steph/sourcerer remove once we have our internal saved object client
// https://github.com/elastic/security-team/issues/1978
indexPatterns: ['read'],
savedObjectsManagement: ['read'],
},
spaces: ['*'],
},
],
},
};
export const secReadCasesAllUser: User = {
username: 'sec_read_cases_all_user',
password: 'password',
roles: [secReadCasesAll.name],
};
const getUserInfo = (user: User): UserInfo => ({
username: user.username,
full_name: user.username.replace('_', ' '),
email: `${user.username}@elastic.co`,
});
export const createUsersAndRoles = (users: User[], roles: Role[]) => {
const envUser = getEnvAuth();
for (const role of roles) {
cy.log(`Creating role: ${JSON.stringify(role)}`);
cy.request({
body: role.privileges,
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'PUT',
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
})
.its('status')
.should('eql', 204);
}
for (const user of users) {
const userInfo = getUserInfo(user);
cy.log(`Creating user: ${JSON.stringify(user)}`);
cy.request({
body: {
username: user.username,
password: user.password,
roles: user.roles,
full_name: userInfo.full_name,
email: userInfo.email,
},
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'POST',
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
})
.its('status')
.should('eql', 200);
}
};
export const deleteUsersAndRoles = (users: User[], roles: Role[]) => {
const envUser = getEnvAuth();
for (const user of users) {
cy.log(`Deleting user: ${JSON.stringify(user)}`);
cy.request({
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'DELETE',
url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`),
failOnStatusCode: false,
})
.its('status')
.should('oneOf', [204, 404]);
}
for (const role of roles) {
cy.log(`Deleting role: ${JSON.stringify(role)}`);
cy.request({
headers: { 'kbn-xsrf': 'cypress-creds-via-config' },
method: 'DELETE',
url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`),
failOnStatusCode: false,
})
.its('status')
.should('oneOf', [204, 404]);
}
};

View file

@ -41,3 +41,4 @@ export const OVERVIEW_URL = '/app/security/overview';
export const RULE_CREATION = 'app/security/rules/create';
export const TIMELINES_URL = '/app/security/timelines';
export const TIMELINE_TEMPLATES_URL = '/app/security/timelines/template';
export const LOGOUT_URL = '/logout';

View file

@ -16,8 +16,8 @@ import { UseUrlState } from '../../common/components/url_state';
import { navTabs } from './home_navigations';
import {
useInitSourcerer,
useSourcererScope,
getScopeFromPath,
useSourcererDataView,
} from '../../common/containers/sourcerer';
import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_security_packages';
import { GlobalHeader } from './global_header';
@ -38,8 +38,7 @@ const HomePageComponent: React.FC<HomePageProps> = ({
useInitSourcerer(getScopeFromPath(pathname));
const { browserFields, indexPattern } = useSourcererScope(getScopeFromPath(pathname));
const { browserFields, indexPattern } = useSourcererDataView(getScopeFromPath(pathname));
// side effect: this will attempt to upgrade the endpoint package if it is not up to date
// this will run when a user navigates to the Security Solution app and when they navigate between
// tabs in the app. This is useful for keeping the endpoint package as up to date as possible until

View file

@ -8,25 +8,23 @@
/* eslint-disable react/display-name */
import React from 'react';
import { useLocation } from 'react-router-dom';
import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public';
import { AppLeaveHandler } from '../../../../../../../../src/core/public';
import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline';
import { useSourcererScope, getScopeFromPath } from '../../../../common/containers/sourcerer';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { TimelineId } from '../../../../../common/types/timeline';
import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning';
import { Flyout } from '../../../../timelines/components/flyout';
import { useResolveRedirect } from '../../../../common/hooks/use_resolve_redirect';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar';
export const SecuritySolutionBottomBar = React.memo(
({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => {
const { pathname } = useLocation();
const [showTimeline] = useShowTimeline();
const { indicesExist } = useSourcererScope(getScopeFromPath(pathname));
const { indicesExist } = useSourcererDataView(SourcererScopeName.timeline);
useResolveRedirect();
return indicesExist && showTimeline ? (

View file

@ -7,7 +7,7 @@
import { isObject, get, isString, isNumber } from 'lodash';
import { useMemo } from 'react';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query';
import { Ecs } from '../../../../../cases/common';
@ -102,7 +102,7 @@ export interface Alert {
[key: string]: unknown;
}
export const useFetchAlertData = (alertIds: string[]): [boolean, Record<string, Ecs>] => {
const { selectedPatterns } = useSourcererScope(SourcererScopeName.detections);
const { selectedPatterns } = useSourcererDataView(SourcererScopeName.detections);
const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]);
const { loading: isLoadingAlerts, data: alertsData } = useQueryAlerts<SignalHit, unknown>({

View file

@ -21,7 +21,7 @@ import { SecurityPageName } from '../../../app/types';
import { useKibana } from '../../../common/lib/kibana';
import { APP_UI_ID } from '../../../../common/constants';
import { timelineActions } from '../../../timelines/store/timeline';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { InvestigateInTimelineAction } from '../../../detections/components/alerts_table/timeline_actions/investigate_in_timeline_action';
@ -53,14 +53,16 @@ export interface CaseProps extends Props {
}
const TimelineDetailsPanel = () => {
const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.detections);
const { browserFields, docValueFields, runtimeMappings } = useSourcererDataView(
SourcererScopeName.detections
);
return (
<DetailsPanel
browserFields={browserFields}
docValueFields={docValueFields}
entityType="events"
isFlyoutView
runtimeMappings={runtimeMappings}
timelineId={TimelineId.casePage}
/>
);
@ -134,6 +136,7 @@ export const CaseView = React.memo(
timelineActions.createTimeline({
id: TimelineId.casePage,
columns: [],
dataViewId: '',
indexNames: [],
expandedDetail: {},
show: false,

View file

@ -125,6 +125,7 @@ exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] =
"packetbeat",
],
"name": "@timestamp",
"readFromDocValues": true,
"searchable": true,
"type": "date",
},

View file

@ -676,7 +676,7 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1`
<div
data-test-subj="more-actions-source.ip"
field="source.ip"
items="[object Object]"
items="[object Object],[object Object]"
value="185.156.74.3"
>
Overflow button
@ -1368,7 +1368,7 @@ exports[`AlertSummaryView Memory event code renders additional summary rows 1`]
<div
data-test-subj="more-actions-source.ip"
field="source.ip"
items="[object Object]"
items="[object Object],[object Object]"
value="185.156.74.3"
>
Overflow button

View file

@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiText, EuiToolTip } from '@elast
import { isEmpty } from 'lodash';
import * as i18n from '../translations';
import { FieldIcon } from '../../../../../../../../src/plugins/kibana_react/public';
import { IndexPatternField } from '../../../../../../../../src/plugins/data/public';
import { DataViewField } from '../../../../../../../../src/plugins/data_views/common';
import { getExampleText } from '../helpers';
import { BrowserField } from '../../../containers/source';
import { EventFieldsData } from '../types';
@ -20,7 +20,7 @@ export interface FieldNameCellProps {
data: EventFieldsData;
field: string;
fieldFromBrowserField: BrowserField;
fieldMapping?: IndexPatternField;
fieldMapping?: DataViewField;
scripted?: boolean;
}
export const FieldNameCell = React.memo(

View file

@ -30,6 +30,7 @@ const hostIpData: EventFieldsData = {
isObjectArray: false,
name: 'host.ip',
originalValue: ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'],
readFromDocValues: false,
searchable: true,
type: 'ip',
values: ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'],
@ -101,6 +102,7 @@ describe('FieldValueCell', () => {
isObjectArray: false,
name: 'message',
originalValue: ['Endpoint network event'],
readFromDocValues: false,
searchable: true,
type: 'string',
values: ['Endpoint network event'],
@ -117,6 +119,7 @@ describe('FieldValueCell', () => {
format: '',
indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'],
name: 'message',
readFromDocValues: false,
searchable: true,
type: 'string',
};
@ -156,6 +159,7 @@ describe('FieldValueCell', () => {
format: '',
indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'],
name: 'host.ip',
readFromDocValues: false,
searchable: true,
type: 'ip',
};

View file

@ -16,8 +16,12 @@ import { mockEventViewerResponse, mockEventViewerResponseWithEvents } from './mo
import { StatefulEventsViewer } from '.';
import { EventsViewer } from './events_viewer';
import { defaultHeaders } from './default_headers';
import { useSourcererScope } from '../../containers/sourcerer';
import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock';
import { useSourcererDataView } from '../../containers/sourcerer';
import {
mockBrowserFields,
mockDocValueFields,
mockRuntimeMappings,
} from '../../containers/source/mock';
import { eventsDefaultModel } from './default_model';
import { useMountAppended } from '../../utils/use_mount_appended';
import { inputsModel } from '../../store/inputs';
@ -91,7 +95,7 @@ jest.mock('../../../timelines/containers', () => ({
jest.mock('../../components/url_state/normalize_time_range.ts');
const mockUseSourcererScope: jest.Mock = useSourcererScope as jest.Mock;
const mockUseSourcererDataView: jest.Mock = useSourcererDataView as jest.Mock;
jest.mock('../../containers/sourcerer');
const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock;
@ -107,6 +111,7 @@ const to = '2019-08-27T22:10:56.794Z';
const defaultMocks = {
browserFields: mockBrowserFields,
docValueFields: mockDocValueFields,
runtimeMappings: mockRuntimeMappings,
indexPattern: mockIndexPattern,
loading: false,
selectedPatterns: mockIndexNames,
@ -139,6 +144,7 @@ const eventsViewerDefaultProps = {
},
renderCellValue: DefaultCellRenderer,
rowRenderers: defaultRowRenderers,
runtimeMappings: {},
start: from,
sort: [
{
@ -169,7 +175,7 @@ describe('EventsViewer', () => {
mockUseTimelineEvents.mockReset();
});
beforeAll(() => {
mockUseSourcererScope.mockImplementation(() => defaultMocks);
mockUseSourcererDataView.mockImplementation(() => defaultMocks);
});
describe('event details', () => {
@ -278,7 +284,7 @@ describe('EventsViewer', () => {
describe('loading', () => {
beforeAll(() => {
mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true }));
mockUseSourcererDataView.mockImplementation(() => ({ ...defaultMocks, loading: true }));
});
beforeEach(() => {
mockUseTimelineEvents.mockReturnValue([false, mockEventViewerResponse]);

View file

@ -11,6 +11,8 @@ import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { useDispatch } from 'react-redux';
import { DataViewBase, Filter, Query } from '@kbn/es-query';
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { Direction } from '../../../../common/search_strategy';
import { BrowserFields, DocValueFields } from '../../containers/source';
import { useTimelineEvents } from '../../../timelines/containers';
@ -30,12 +32,7 @@ import {
import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline';
import { EventDetailsWidthProvider } from './event_details_width_context';
import * as i18n from './translations';
import {
Filter,
esQuery,
IIndexPattern,
Query,
} from '../../../../../../../src/plugins/data/public';
import { esQuery } from '../../../../../../../src/plugins/data/public';
import { inputsModel } from '../../store';
import { ExitFullScreen } from '../exit_full_screen';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
@ -123,7 +120,7 @@ interface Props {
headerFilterGroup?: React.ReactNode;
id: TimelineId;
indexNames: string[];
indexPattern: IIndexPattern;
indexPattern: DataViewBase;
isLive: boolean;
isLoadingIndexPattern: boolean;
itemsPerPage: number;
@ -133,6 +130,7 @@ interface Props {
onRuleChange?: () => void;
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
rowRenderers: RowRenderer[];
runtimeMappings: MappingRuntimeFields;
start: string;
sort: Sort[];
showTotalCount?: boolean;
@ -162,6 +160,7 @@ const EventsViewerComponent: React.FC<Props> = ({
query,
renderCellValue,
rowRenderers,
runtimeMappings,
start,
sort,
showTotalCount = true,
@ -240,6 +239,7 @@ const EventsViewerComponent: React.FC<Props> = ({
id,
indexNames,
limit: itemsPerPage,
runtimeMappings,
sort: sortField,
startDate: start,
endDate: end,

View file

@ -22,7 +22,7 @@ import { InspectButtonContainer } from '../inspect';
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { useSourcererDataView } from '../../containers/sourcerer';
import type { EntityType } from '../../../../../timelines/common';
import { TGridCellAction } from '../../../../../timelines/common/types';
import { DetailsPanel } from '../../../timelines/components/side_panel';
@ -117,9 +117,12 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
browserFields,
docValueFields,
indexPattern,
runtimeMappings,
selectedPatterns,
dataViewId: selectedDataViewId,
loading: isLoadingIndexPattern,
} = useSourcererScope(scopeId);
} = useSourcererDataView(scopeId);
const { globalFullScreen } = useGlobalFullScreen();
// TODO: Once we are past experimental phase this code should be removed
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
@ -129,14 +132,15 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
useEffect(() => {
if (createTimeline != null) {
createTimeline({
id,
columns,
dataViewId: selectedDataViewId,
defaultColumns,
excludedRowRendererIds,
id,
indexNames: selectedPatterns,
sort,
itemsPerPage,
showCheckboxes,
sort,
});
}
return () => {
@ -206,6 +210,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
query,
renderCellValue,
rowRenderers,
runtimeMappings,
setQuery,
sort,
start,
@ -235,6 +240,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
onRuleChange={onRuleChange}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
runtimeMappings={runtimeMappings}
start={start}
sort={sort}
showTotalCount={isEmpty(graphEventId) ? true : false}
@ -249,6 +255,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
entityType={entityType}
docValueFields={docValueFields}
isFlyoutView
runtimeMappings={runtimeMappings}
timelineId={id}
/>
</>

View file

@ -42,13 +42,13 @@ import { getCommentsArrayMock } from '../../../../../lists/common/schemas/types/
import { fields } from '../../../../../../../src/plugins/data/common/mocks';
import { ENTRIES, OLD_DATE_RELATIVE_TO_DATE_NOW } from '../../../../../lists/common/constants.mock';
import { CodeSignature } from '../../../../common/ecs/file';
import { IndexPatternBase } from '@kbn/es-query';
import { DataViewBase } from '@kbn/es-query';
jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('123'),
}));
const getMockIndexPattern = (): IndexPatternBase => ({
const getMockIndexPattern = (): DataViewBase => ({
fields,
id: '1234',
title: 'logstash-*',
@ -364,7 +364,7 @@ describe('Exception helpers', () => {
name: 'nested.field',
},
],
} as IndexPatternBase;
} as DataViewBase;
test('it should return false with an empty array', () => {
const payload: ExceptionListItemSchema[] = [];

View file

@ -33,7 +33,7 @@ import {
addIdToEntries,
ExceptionsBuilderExceptionItem,
} from '@kbn/securitysolution-list-utils';
import { IndexPatternBase } from '@kbn/es-query';
import { DataViewBase } from '@kbn/es-query';
import * as i18n from './translations';
import { AlertData, Flattened } from './types';
@ -46,10 +46,10 @@ import exceptionableEndpointFields from './exceptionable_endpoint_fields.json';
import exceptionableEndpointEventFields from './exceptionable_endpoint_event_fields.json';
export const filterIndexPatterns = (
patterns: IndexPatternBase,
patterns: DataViewBase,
type: ExceptionListType,
osTypes?: OsTypeArray
): IndexPatternBase => {
): DataViewBase => {
switch (type) {
case 'endpoint':
const osFilterForEndpoint: (name: string) => boolean = osTypes?.includes('linux')
@ -752,7 +752,7 @@ export const getPrepopulatedBehaviorException = ({
*/
export const entryHasNonEcsType = (
exceptionItems: Array<ExceptionListItemSchema | CreateExceptionListItemSchema>,
indexPatterns: IndexPatternBase
indexPatterns: DataViewBase
): boolean => {
const doesFieldNameExist = (exceptionEntry: Entry): boolean => {
return indexPatterns.fields.some(({ name }) => name === exceptionEntry.field);

View file

@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n';
import { StatefulTopN } from '../../top_n';
import { TimelineId } from '../../../../../common/types/timeline';
import { SourcererScopeName } from '../../../store/sourcerer/model';
import { useSourcererScope } from '../../../containers/sourcerer';
import { useSourcererDataView } from '../../../containers/sourcerer';
import { TooltipWithKeyboardShortcut } from '../../accessibility';
import { getAdditionalScreenReaderOnlyContext } from '../utils';
import { SHOW_TOP_N_KEYBOARD_SHORTCUT } from '../keyboard_shortcut_constants';
@ -85,7 +85,8 @@ export const ShowTopNButton: React.FC<Props> = React.memo(
)
? SourcererScopeName.detections
: SourcererScopeName.default;
const { browserFields, indexPattern } = useSourcererScope(activeScope);
const { browserFields, indexPattern } = useSourcererDataView(activeScope);
const icon = iconType ?? 'visBarVertical';
const side = iconSide ?? 'left';
const buttonTitle = title ?? SHOW_TOP(field);

View file

@ -13,7 +13,7 @@ import { DataProvider } from '../../../../common/types/timeline';
jest.mock('../../lib/kibana');
jest.mock('../../hooks/use_selector');
jest.mock('../../containers/sourcerer', () => ({
useSourcererScope: jest.fn().mockReturnValue({ browserFields: {} }),
useSourcererDataView: jest.fn().mockReturnValue({ browserFields: {} }),
}));
describe('useHoverActionItems', () => {

View file

@ -17,7 +17,7 @@ import { allowTopN } from '../drag_and_drop/helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { ColumnHeaderOptions, DataProvider, TimelineId } from '../../../../common/types/timeline';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererScope } from '../../containers/sourcerer';
import { useSourcererDataView } from '../../containers/sourcerer';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { ShowTopNButton } from './actions/show_top_n';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
@ -116,7 +116,7 @@ export const useHoverActionItems = ({
)
? SourcererScopeName.detections
: SourcererScopeName.default;
const { browserFields } = useSourcererScope(activeScope);
const { browserFields } = useSourcererDataView(activeScope);
/*
* In the case of `DisableOverflowButton`, we show filters only when topN is NOT opened. As after topN button is clicked, the chart panel replace current hover actions in the hover actions' popover, so we have to hide all the actions.

View file

@ -20,7 +20,7 @@ import {
import { Query, Filter } from '../../../../../../../src/plugins/data/public';
import { SearchNavTab } from './types';
import { SourcererScopePatterns } from '../../store/sourcerer/model';
import { SourcererUrlState } from '../../store/sourcerer/model';
export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => {
if (tab && tab.urlKey != null && !isAdministration(tab.urlKey)) {
@ -29,7 +29,7 @@ export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => {
let urlStateToReplace:
| Filter[]
| Query
| SourcererScopePatterns
| SourcererUrlState
| TimelineUrl
| UrlInputsModel
| string = '';

View file

@ -7,7 +7,7 @@
import { UrlInputsModel } from '../../../store/inputs/model';
import { CONSTANTS } from '../../url_state/constants';
import { SourcererScopePatterns } from '../../../store/sourcerer/model';
import { SourcererUrlState } from '../../../store/sourcerer/model';
import { TimelineUrl } from '../../../../timelines/store/timeline/model';
import { Filter, Query } from '../../../../../../../../src/plugins/data/public';
@ -21,7 +21,7 @@ export interface TabNavigationProps extends SecuritySolutionTabNavigationProps {
[CONSTANTS.appQuery]?: Query;
[CONSTANTS.filters]?: Filter[];
[CONSTANTS.savedQuery]?: string;
[CONSTANTS.sourcerer]: SourcererScopePatterns;
[CONSTANTS.sourcerer]: SourcererUrlState;
[CONSTANTS.timerange]: UrlInputsModel;
[CONSTANTS.timeline]: TimelineUrl;
}

View file

@ -8,11 +8,9 @@
import React, { memo, useMemo, useCallback } from 'react';
import deepEqual from 'fast-deep-equal';
import { DataViewBase, Filter, Query } from '@kbn/es-query';
import {
Filter,
IIndexPattern,
FilterManager,
Query,
TimeHistory,
TimeRange,
SavedQuery,
@ -26,7 +24,7 @@ export interface QueryBarComponentProps {
dateRangeFrom?: string;
dateRangeTo?: string;
hideSavedQuery?: boolean;
indexPattern: IIndexPattern;
indexPattern: DataViewBase;
isLoading?: boolean;
isRefreshPaused?: boolean;
filterQuery: Query;

View file

@ -6,13 +6,42 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { render, fireEvent } from '@testing-library/react';
import { InputsModelId } from '../../store/inputs/constants';
import { SearchBarComponent } from '.';
import { TestProviders } from '../../mock';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
import { coreMock } from '../../../../../../../src/core/public/mocks';
jest.mock('../../lib/kibana');
const mockFilterManager = new FilterManager(coreMock.createStart().uiSettings);
jest.mock('../../lib/kibana', () => {
const original = jest.requireActual('../../lib/kibana');
return {
useKibana: () => ({
services: {
...original.useKibana().services,
data: {
...original.useKibana().services.data,
query: {
...original.useKibana().services.data.query,
filterManager: mockFilterManager,
},
ui: {
SearchBar: jest.fn().mockImplementation((props) => (
<button
data-test-subj="querySubmitButton"
onClick={() => props.onQuerySubmit({ dateRange: { from: 'now', to: 'now' } })}
type="button"
>
{'Hello world'}
</button>
)),
},
},
},
}),
};
});
describe('SearchBarComponent', () => {
const props = {
@ -37,9 +66,38 @@ describe('SearchBarComponent', () => {
savedQuery: undefined,
};
const pollForSignalIndex = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
});
it('calls setSearchBarFilter on mount', () => {
mount(<SearchBarComponent {...props} />, { wrappingComponent: TestProviders });
render(
<TestProviders>
<SearchBarComponent {...props} />
</TestProviders>
);
expect(props.setSearchBarFilter).toHaveBeenCalled();
});
it('calls pollForSignalIndex on Refresh button click', () => {
const { getByTestId } = render(
<TestProviders>
<SearchBarComponent {...props} pollForSignalIndex={pollForSignalIndex} />
</TestProviders>
);
fireEvent.click(getByTestId('querySubmitButton'));
expect(pollForSignalIndex).toHaveBeenCalled();
});
it('does not call pollForSignalIndex on Refresh button click if pollForSignalIndex not passed', () => {
const { getByTestId } = render(
<TestProviders>
<SearchBarComponent {...props} />
</TestProviders>
);
fireEvent.click(getByTestId('querySubmitButton'));
expect(pollForSignalIndex).not.toHaveBeenCalled();
});
});

View file

@ -13,14 +13,9 @@ import { Dispatch } from 'redux';
import { Subscription } from 'rxjs';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import {
FilterManager,
IIndexPattern,
TimeRange,
Query,
Filter,
SavedQuery,
} from 'src/plugins/data/public';
import { DataViewBase, Filter, Query } from '@kbn/es-query';
import { FilterManager, TimeRange, SavedQuery } from 'src/plugins/data/public';
import { OnTimeChangeProps } from '@elastic/eui';
@ -48,7 +43,8 @@ const APP_STATE_STORAGE_KEY = 'securitySolution.searchBar.appState';
interface SiemSearchBarProps {
id: InputsModelId;
indexPattern: IIndexPattern;
indexPattern: DataViewBase;
pollForSignalIndex?: () => void;
timelineId?: string;
dataTestSubj?: string;
}
@ -67,6 +63,7 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
id,
indexPattern,
isLoading = false,
pollForSignalIndex,
queries,
savedQuery,
setSavedQuery,
@ -100,6 +97,11 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
const onQuerySubmit = useCallback(
(payload: { dateRange: TimeRange; query?: Query }) => {
// if the function is there, call it to check if the signals index exists yet
// in order to update the index fields
if (pollForSignalIndex != null) {
pollForSignalIndex();
}
const isQuickSelection =
payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now');
let updateSearchBar: UpdateReduxSearchBar = {
@ -144,7 +146,18 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
window.setTimeout(() => updateSearch(updateSearchBar), 0);
},
[id, toStr, end, fromStr, start, filterManager, filterQuery, queries, updateSearch]
[
id,
pollForSignalIndex,
toStr,
end,
fromStr,
start,
filterManager,
filterQuery,
queries,
updateSearch,
]
);
const onRefresh = useCallback(

View file

@ -6,13 +6,9 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { mount, ReactWrapper } from 'enzyme';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { Sourcerer } from './index';
import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
import { sourcererActions, sourcererModel } from '../../store/sourcerer';
import {
createSecuritySolutionStorageMock,
@ -22,6 +18,7 @@ import {
TestProviders,
} from '../../mock';
import { createStore, State } from '../../store';
import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
@ -49,16 +46,28 @@ const defaultProps = {
};
describe('Sourcerer component', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
});
const state: State = mockGlobalState;
const { id, patternList, title } = state.sourcerer.defaultDataView;
const patternListNoSignals = patternList
.filter((p) => p !== state.sourcerer.signalIndexName)
.sort();
const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({
availableOptionCount: wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).length,
optionsSelected: patterns.every((pattern) =>
wrapper
.find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`)
.first()
.exists()
),
});
const { storage } = createSecuritySolutionStorageMock();
let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
beforeEach(() => {
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
jest.clearAllMocks();
jest.restoreAllMocks();
});
it('renders tooltip', () => {
@ -99,72 +108,43 @@ describe('Sourcerer component', () => {
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
expect(
wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('selectedOptions')
).toEqual(mockOptions);
});
it('Mounts with some options selected', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
selectedPatterns: [DEFAULT_INDEX_PATTERN[0]],
},
},
},
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
expect(
wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('selectedOptions')
).toEqual([mockOptions[0]]);
});
it('onChange calls updateSourcererScopeIndices', async () => {
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
expect(
wrapper.find(`[data-test-subj="sourcerer-popover"]`).first().prop('isOpen')
).toBeTruthy();
await waitFor(() => {
(
wrapper.find(EuiComboBox).props() as unknown as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}
).onChange([mockOptions[0], mockOptions[1]]);
wrapper.update();
});
wrapper.find(`[data-test-subj="add-index"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="sourcerer-popover"]`).first().prop('isOpen')).toBeFalsy();
expect(mockDispatch).toHaveBeenCalledWith(
sourcererActions.setSelectedIndexPatterns({
id: SourcererScopeName.default,
selectedPatterns: [mockOptions[0].value, mockOptions[1].value],
})
wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('selectedOptions')
).toEqual(
patternListNoSignals.map((p) => ({
label: p,
value: p,
}))
);
});
it('resets to config index patterns', async () => {
it('Removes duplicate options from title', () => {
store = createStore(
{
...state,
sourcerer: {
...state.sourcerer,
kibanaIndexPatterns: [{ id: '1234', title: 'auditbeat-*' }],
configIndexPatterns: ['packetbeat-*'],
defaultDataView: {
...state.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
kibanaDataViews: [
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
],
sourcererScopes: {
...state.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
selectedDataViewId: '1234',
selectedPatterns: ['filebeat-*'],
},
},
},
},
SUB_PLUGINS_REDUCER,
@ -177,16 +157,252 @@ describe('Sourcerer component', () => {
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy();
wrapper
.find(`[data-test-subj="sourcerer-combo-box"] [data-test-subj="comboBoxToggleListButton"]`)
.first()
.simulate('click');
const options: Array<EuiSuperSelectOption<string>> = wrapper
.find(`[data-test-subj="sourcerer-combo-box"]`)
.first()
.prop('options');
expect(options.length).toEqual(2);
});
it('Disables options with no data', () => {
store = createStore(
{
...state,
sourcerer: {
...state.sourcerer,
defaultDataView: {
...state.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,fakebeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
kibanaDataViews: [
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*,auditbeat-*,fakebeat-*',
patternList: ['filebeat-*', 'auditbeat-*'],
},
],
sourcererScopes: {
...state.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
selectedDataViewId: '1234',
selectedPatterns: ['filebeat-*'],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper
.find(`[data-test-subj="sourcerer-combo-box"] [data-test-subj="comboBoxToggleListButton"]`)
.first()
.simulate('click');
const options: Array<EuiSuperSelectOption<string>> = wrapper
.find(`[data-test-subj="sourcerer-combo-box"]`)
.first()
.prop('options');
const disabledOption = options.find((o) => o.disabled);
expect(disabledOption?.value).toEqual('fakebeat-*');
});
it('Mounts with multiple options selected - default', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
...state.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
patternList,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 2),
},
},
},
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({
// should hide signal index
availableOptionCount: title.split(',').length - 3,
optionsSelected: true,
});
});
it('Mounts with multiple options selected - timeline', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
...state.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
loading: false,
patternList,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 2),
},
},
},
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({
// should show every option except fakebeat-*
availableOptionCount: title.split(',').length - 2,
optionsSelected: true,
});
});
it('onSave dispatches setSelectedDataView', async () => {
store = createStore(
{
...state,
sourcerer: {
...state.sourcerer,
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*',
patternList: ['filebeat-*'],
},
],
sourcererScopes: {
...state.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 2),
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({
availableOptionCount: title.split(',').length - 3,
optionsSelected: true,
});
wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 3))).toEqual({
availableOptionCount: title.split(',').length - 4,
optionsSelected: true,
});
wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="sourcerer-popover"]`).first().prop('isOpen')).toBeFalsy();
expect(mockDispatch).toHaveBeenCalledWith(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.default,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 3),
})
);
});
it('resets to default index pattern', async () => {
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, patternListNoSignals)).toEqual({
availableOptionCount: 1,
optionsSelected: true,
});
wrapper
.find(
`[data-test-subj="indexPattern-switcher"] [title="packetbeat-*"] button.euiBadge__iconButton`
`[data-test-subj="sourcerer-combo-box"] [title="${patternList[0]}"] button.euiBadge__iconButton`
)
.first()
.simulate('click');
expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeTruthy();
expect(
checkOptionsAndSelections(wrapper, patternListNoSignals.slice(1, patternListNoSignals.length))
).toEqual({
availableOptionCount: 2,
optionsSelected: true,
});
wrapper.find(`[data-test-subj="sourcerer-reset"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy();
expect(checkOptionsAndSelections(wrapper, patternListNoSignals)).toEqual({
availableOptionCount: 1,
optionsSelected: true,
});
});
it('disables saving when no index patterns are selected', () => {
store = createStore(
@ -194,7 +410,15 @@ describe('Sourcerer component', () => {
...state,
sourcerer: {
...state.sourcerer,
kibanaIndexPatterns: [{ id: '1234', title: 'auditbeat-*' }],
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
],
},
},
SUB_PLUGINS_REDUCER,
@ -208,78 +432,152 @@ describe('Sourcerer component', () => {
);
wrapper.find('[data-test-subj="sourcerer-trigger"]').first().simulate('click');
wrapper.find('[data-test-subj="comboBoxClearButton"]').first().simulate('click');
expect(wrapper.find('[data-test-subj="add-index"]').first().prop('disabled')).toBeTruthy();
expect(wrapper.find('[data-test-subj="sourcerer-save"]').first().prop('disabled')).toBeTruthy();
});
it('returns index pattern options for kibanaIndexPatterns and configIndexPatterns', () => {
store = createStore(
{
...state,
sourcerer: {
...state.sourcerer,
kibanaIndexPatterns: [{ id: '1234', title: 'auditbeat-*' }],
configIndexPatterns: ['packetbeat-*'],
it('Selects a different index pattern', async () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'fakebeat-*,neatbeat-*',
patternList: ['fakebeat-*'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
patternList,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 2),
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="config-option"]`).first().exists()).toBeFalsy();
wrapper
.find(
`[data-test-subj="indexPattern-switcher"] [title="auditbeat-*"] button.euiBadge__iconButton`
)
.first()
.simulate('click');
wrapper.update();
expect(wrapper.find(`[data-test-subj="kip-option"]`).first().text()).toEqual(' auditbeat-*');
wrapper
.find(
`[data-test-subj="indexPattern-switcher"] [title="packetbeat-*"] button.euiBadge__iconButton`
)
.first()
.simulate('click');
wrapper.update();
expect(wrapper.find(`[data-test-subj="config-option"]`).first().text()).toEqual('packetbeat-*');
wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click');
wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click');
expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({
availableOptionCount: 0,
optionsSelected: true,
});
wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click');
expect(mockDispatch).toHaveBeenCalledWith(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.default,
selectedDataViewId: '1234',
selectedPatterns: ['fakebeat-*'],
})
);
});
it('combines index pattern options for kibanaIndexPatterns and configIndexPatterns', () => {
store = createStore(
{
...state,
sourcerer: {
...state.sourcerer,
kibanaIndexPatterns: [
{ id: '1234', title: 'auditbeat-*' },
{ id: '5678', title: 'packetbeat-*' },
],
configIndexPatterns: ['packetbeat-*'],
it('Does display signals index on timeline sourcerer', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
...state.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
loading: false,
patternList,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 2),
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer scope={sourcererModel.SourcererScopeName.timeline} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxToggleListButton"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).at(6).text()).toEqual(
mockGlobalState.sourcerer.signalIndexName
);
});
it('Does not display signals index on default sourcerer', () => {
const state2 = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
state.sourcerer.defaultDataView,
{
...state.sourcerer.defaultDataView,
id: '1234',
title: 'auditbeat-*',
patternList: ['auditbeat-*'],
},
{
...state.sourcerer.defaultDataView,
id: '12347',
title: 'packetbeat-*',
patternList: ['packetbeat-*'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
loading: false,
patternList,
selectedDataViewId: id,
selectedPatterns: patternList.slice(0, 2),
},
},
},
};
store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper
.find(
`[data-test-subj="indexPattern-switcher"] [title="packetbeat-*"] button.euiBadge__iconButton`
)
.first()
.simulate('click');
wrapper.update();
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
expect(
wrapper.find(`[title="packetbeat-*"] [data-test-subj="kip-option"]`).first().text()
).toEqual(' packetbeat-*');
wrapper
.find(
`[data-test-subj="sourcerer-combo-box"] span[title="${mockGlobalState.sourcerer.signalIndexName}"]`
)
.first()
.exists()
).toBeFalsy();
});
});

View file

@ -10,24 +10,26 @@ import {
EuiButtonEmpty,
EuiComboBox,
EuiComboBoxOptionOption,
EuiIcon,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPopover,
EuiPopoverTitle,
EuiSpacer,
EuiSuperSelect,
EuiText,
EuiToolTip,
} from '@elastic/eui';
import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import * as i18n from './translations';
import { sourcererActions, sourcererModel } from '../../store/sourcerer';
import { State } from '../../store';
import { getSourcererScopeSelector, SourcererScopeSelector } from './selectors';
import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer';
import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { SourcererScopeName } from '../../store/sourcerer/model';
const PopoverContent = styled.div`
width: 600px;
@ -40,30 +42,77 @@ interface SourcererComponentProps {
scope: sourcererModel.SourcererScopeName;
}
const getPatternListWithoutSignals = (
patternList: string[],
signalIndexName: string | null
): string[] => patternList.filter((p) => p !== signalIndexName);
export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }) => {
const dispatch = useDispatch();
const sourcererScopeSelector = useMemo(getSourcererScopeSelector, []);
const { configIndexPatterns, kibanaIndexPatterns, sourcererScope } = useSelector<
State,
SourcererScopeSelector
>((state) => sourcererScopeSelector(state, scopeId), deepEqual);
const { selectedPatterns, loading } = sourcererScope;
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
const {
defaultDataView,
kibanaDataViews,
signalIndexName,
sourcererScope: { selectedDataViewId, selectedPatterns, loading },
} = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId));
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
const [dataViewId, setDataViewId] = useState<string>(selectedDataViewId ?? defaultDataView.id);
const { patternList, selectablePatterns } = useMemo(() => {
const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId);
return theDataView != null
? scopeId === SourcererScopeName.default
? {
patternList: getPatternListWithoutSignals(
theDataView.title
.split(',')
// remove duplicates patterns from selector
.filter((pattern, i, self) => self.indexOf(pattern) === i),
signalIndexName
),
selectablePatterns: getPatternListWithoutSignals(
theDataView.patternList,
signalIndexName
),
}
: {
patternList: theDataView.title
.split(',')
// remove duplicates patterns from selector
.filter((pattern, i, self) => self.indexOf(pattern) === i),
selectablePatterns: theDataView.patternList,
}
: { patternList: [], selectablePatterns: [] };
}, [kibanaDataViews, scopeId, signalIndexName, dataViewId]);
const selectableOptions = useMemo(
() =>
patternList.map((indexName) => ({
label: indexName,
value: indexName,
disabled: !selectablePatterns.includes(indexName),
})),
[selectablePatterns, patternList]
);
const [selectedOptions, setSelectedOptions] = useState<Array<EuiComboBoxOptionOption<string>>>(
selectedPatterns.map((indexSelected) => ({
label: indexSelected,
value: indexSelected,
selectedPatterns.map((indexName) => ({
label: indexName,
value: indexName,
}))
);
const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]);
const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []);
const onChangeIndexPattern = useCallback(
(newSelectedPatterns: string[]) => {
const onChangeDataView = useCallback(
(newSelectedDataView: string, newSelectedPatterns: string[]) => {
dispatch(
sourcererActions.setSelectedIndexPatterns({
sourcererActions.setSelectedDataView({
id: scopeId,
selectedDataViewId: newSelectedDataView,
selectedPatterns: newSelectedPatterns,
})
);
@ -72,52 +121,55 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
);
const renderOption = useCallback(
({ value }) =>
kibanaIndexPatterns.some((kip) => kip.title === value) ? (
<span data-test-subj="kip-option">
<EuiIcon type="logoKibana" size="s" /> {value}
</span>
) : (
<span data-test-subj="config-option">{value}</span>
),
[kibanaIndexPatterns]
({ value }) => <span data-test-subj="sourcerer-combo-option">{value}</span>,
[]
);
const onChangeCombo = useCallback((newSelectedOptions) => {
setSelectedOptions(newSelectedOptions);
}, []);
const onChangeSuper = useCallback(
(newSelectedOption) => {
setDataViewId(newSelectedOption);
setSelectedOptions(
getScopePatternListSelection(
kibanaDataViews.find((dataView) => dataView.id === newSelectedOption),
scopeId,
signalIndexName,
newSelectedOption === defaultDataView.id
).map((indexSelected: string) => ({
label: indexSelected,
value: indexSelected,
}))
);
},
[defaultDataView.id, kibanaDataViews, scopeId, signalIndexName]
);
const resetDataSources = useCallback(() => {
setDataViewId(defaultDataView.id);
setSelectedOptions(
configIndexPatterns.map((indexSelected) => ({
label: indexSelected,
value: indexSelected,
}))
getScopePatternListSelection(defaultDataView, scopeId, signalIndexName, true).map(
(indexSelected: string) => ({
label: indexSelected,
value: indexSelected,
})
)
);
}, [configIndexPatterns]);
}, [defaultDataView, scopeId, signalIndexName]);
const handleSaveIndices = useCallback(() => {
onChangeIndexPattern(selectedOptions.map((so) => so.label));
onChangeDataView(
dataViewId,
selectedOptions.map((so) => so.label)
);
setPopoverIsOpen(false);
}, [onChangeIndexPattern, selectedOptions]);
}, [onChangeDataView, dataViewId, selectedOptions]);
const handleClosePopOver = useCallback(() => {
setPopoverIsOpen(false);
}, []);
const indexesPatternOptions = useMemo(
() =>
[...configIndexPatterns, ...kibanaIndexPatterns.map((kip) => kip.title)].reduce<
Array<EuiComboBoxOptionOption<string>>
>((acc, index) => {
if (index != null && !acc.some((o) => o.label.includes(index))) {
return [...acc, { label: index, value: index }];
}
return acc;
}, []),
[configIndexPatterns, kibanaIndexPatterns]
);
const trigger = useMemo(
() => (
<EuiButtonEmpty
@ -136,37 +188,43 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
[setPopoverIsOpenCb, loading]
);
const comboBox = useMemo(
() => (
<EuiComboBox
data-test-subj="indexPattern-switcher"
placeholder={i18n.PICK_INDEX_PATTERNS}
fullWidth
options={indexesPatternOptions}
selectedOptions={selectedOptions}
onChange={onChangeCombo}
renderOption={renderOption}
/>
),
[indexesPatternOptions, onChangeCombo, renderOption, selectedOptions]
const dataViewSelectOptions = useMemo(
() =>
kibanaDataViews.map(({ title, id }) => ({
inputDisplay:
id === defaultDataView.id ? (
<span data-test-subj="security-option-super">
<EuiIcon type="logoSecurity" size="s" /> {i18n.SIEM_DATA_VIEW_LABEL}
</span>
) : (
<span data-test-subj="dataView-option-super">
<EuiIcon type="logoKibana" size="s" /> {title}
</span>
),
value: id,
})),
[defaultDataView.id, kibanaDataViews]
);
useEffect(() => {
const newSelecteOptions = selectedPatterns.map((indexSelected) => ({
label: indexSelected,
value: indexSelected,
}));
setSelectedOptions((prevSelectedOptions) => {
if (!deepEqual(newSelecteOptions, prevSelectedOptions)) {
return newSelecteOptions;
}
return prevSelectedOptions;
});
setDataViewId((prevSelectedOption) =>
selectedDataViewId != null && !deepEqual(selectedDataViewId, prevSelectedOption)
? selectedDataViewId
: prevSelectedOption
);
}, [selectedDataViewId]);
useEffect(() => {
setSelectedOptions(
selectedPatterns.map((indexName) => ({
label: indexName,
value: indexName,
}))
);
}, [selectedPatterns]);
const tooltipContent = useMemo(
() => (isPopoverOpen ? null : sourcererScope.selectedPatterns.sort().join(', ')),
[isPopoverOpen, sourcererScope.selectedPatterns]
() => (isPopoverOpen ? null : selectedPatterns.join(', ')),
[selectedPatterns, isPopoverOpen]
);
const buttonWithTooptip = useMemo(() => {
@ -196,7 +254,24 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
<EuiSpacer size="s" />
<EuiText color="default">{i18n.INDEX_PATTERNS_SELECTION_LABEL}</EuiText>
<EuiSpacer size="xs" />
{comboBox}
<EuiSuperSelect
data-test-subj="sourcerer-select"
placeholder={i18n.PICK_INDEX_PATTERNS}
fullWidth
options={dataViewSelectOptions}
valueOfSelected={dataViewId}
onChange={onChangeSuper}
/>
<EuiSpacer size="xs" />
<EuiComboBox
data-test-subj="sourcerer-combo-box"
fullWidth
onChange={onChangeCombo}
options={selectableOptions}
placeholder={i18n.PICK_INDEX_PATTERNS}
renderOption={renderOption}
selectedOptions={selectedOptions}
/>
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>
@ -214,7 +289,7 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
<EuiButton
onClick={handleSaveIndices}
disabled={isSavingDisabled}
data-test-subj="add-index"
data-test-subj="sourcerer-save"
fill
fullWidth
size="s"

View file

@ -1,36 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { State } from '../../store';
import { sourcererSelectors } from '../../store/sourcerer';
import { KibanaIndexPatterns, ManageScope, SourcererScopeName } from '../../store/sourcerer/model';
export interface SourcererScopeSelector {
configIndexPatterns: string[];
kibanaIndexPatterns: KibanaIndexPatterns;
sourcererScope: ManageScope;
}
export const getSourcererScopeSelector = () => {
const getKibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector();
const getScopesSelector = sourcererSelectors.scopesSelector();
const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector();
const mapStateToProps = (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => {
const kibanaIndexPatterns = getKibanaIndexPatternsSelector(state);
const scope = getScopesSelector(state)[scopeId];
const configIndexPatterns = getConfigIndexPatternsSelector(state);
return {
kibanaIndexPatterns,
configIndexPatterns,
sourcererScope: scope,
};
};
return mapStateToProps;
};

View file

@ -11,9 +11,12 @@ export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.da
defaultMessage: 'Data sources',
});
export const ALL_DEFAULT = i18n.translate('xpack.securitySolution.indexPatterns.allDefault', {
defaultMessage: 'All default',
});
export const SIEM_DATA_VIEW_LABEL = i18n.translate(
'xpack.securitySolution.indexPatterns.kipLabel',
{
defaultMessage: 'Default Security Data View',
}
);
export const SELECT_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', {
defaultMessage: 'Data sources selection',

View file

@ -11,7 +11,7 @@ import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { EntryItem } from './entry_item';
import { fields, getField } from '../../../../../../../src/plugins/data/common/mocks';
import { IndexPattern } from 'src/plugins/data/public';
import { DataViewBase } from '@kbn/es-query';
jest.mock('../../../common/lib/kibana');
@ -31,7 +31,7 @@ describe('EntryItem', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
showLabel={true}
onChange={jest.fn()}
@ -40,7 +40,7 @@ describe('EntryItem', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
/>
);
@ -64,14 +64,14 @@ describe('EntryItem', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
showLabel={false}
onChange={mockOnChange}
@ -111,14 +111,14 @@ describe('EntryItem', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
showLabel={false}
onChange={mockOnChange}

View file

@ -10,16 +10,15 @@ import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { FieldComponent } from '@kbn/securitysolution-autocomplete';
import { IndexPatternFieldBase } from '@kbn/es-query';
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
import { FormattedEntry, Entry } from './types';
import * as i18n from './translations';
import { getEntryOnFieldChange, getEntryOnThreatFieldChange } from './helpers';
interface EntryItemProps {
entry: FormattedEntry;
indexPattern: IndexPattern;
threatIndexPatterns: IndexPattern;
indexPattern: DataViewBase;
threatIndexPatterns: DataViewBase;
showLabel: boolean;
onChange: (arg: Entry, i: number) => void;
}
@ -41,7 +40,7 @@ export const EntryItem: React.FC<EntryItemProps> = ({
onChange,
}): JSX.Element => {
const handleFieldChange = useCallback(
([newField]: IndexPatternFieldBase[]): void => {
([newField]: DataViewFieldBase[]): void => {
const { updatedEntry, index } = getEntryOnFieldChange(entry, newField);
onChange(updatedEntry, index);
},
@ -49,7 +48,7 @@ export const EntryItem: React.FC<EntryItemProps> = ({
);
const handleThreatFieldChange = useCallback(
([newField]: IndexPatternFieldBase[]): void => {
([newField]: DataViewFieldBase[]): void => {
const { updatedEntry, index } = getEntryOnThreatFieldChange(entry, newField);
onChange(updatedEntry, index);
},

View file

@ -7,7 +7,8 @@
import { fields, getField } from '../../../../../../../src/plugins/data/common/mocks';
import { Entry, EmptyEntry, ThreatMapEntries, FormattedEntry } from './types';
import { FieldSpec, IndexPattern } from '../../../../../../../src/plugins/data/common';
import { FieldSpec } from '../../../../../../../src/plugins/data/common';
import { DataViewBase } from '@kbn/es-query';
import moment from 'moment-timezone';
import {
@ -24,12 +25,12 @@ jest.mock('uuid', () => ({
v4: jest.fn().mockReturnValue('123'),
}));
const getMockIndexPattern = (): IndexPattern =>
const getMockIndexPattern = (): DataViewBase =>
({
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern);
} as DataViewBase);
const getMockEntry = (): FormattedEntry => ({
id: '123',
@ -51,7 +52,7 @@ describe('Helpers', () => {
describe('#getFormattedEntry', () => {
test('it returns entry with a value when "item.field" is of type "text" and matching keyword field exists', () => {
const payloadIndexPattern: IndexPattern = {
const payloadIndexPattern: DataViewBase = {
...getMockIndexPattern(),
fields: [
...fields,
@ -66,7 +67,7 @@ describe('Helpers', () => {
readFromDocValues: true,
},
],
} as IndexPattern;
} as DataViewBase;
const payloadItem: Entry = {
field: 'machine.os.raw.text',
type: 'mapping',
@ -171,7 +172,7 @@ describe('Helpers', () => {
});
test('it returns formatted entries', () => {
const payloadIndexPattern: IndexPattern = getMockIndexPattern();
const payloadIndexPattern: DataViewBase = getMockIndexPattern();
const payloadItems: Entry[] = [
{ field: 'machine.os', type: 'mapping', value: 'machine.os' },
{ field: 'ip', type: 'mapping', value: 'ip' },

View file

@ -10,8 +10,7 @@ import { i18n } from '@kbn/i18n';
import { addIdToItem } from '@kbn/securitysolution-utils';
import { ThreatMap, threatMap, ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import { IndexPatternFieldBase } from '@kbn/es-query';
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { DataViewBase, DataViewFieldBase } from '@kbn/es-query';
import { Entry, FormattedEntry, ThreatMapEntries, EmptyEntry } from './types';
import { ValidationFunc } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
import { ERROR_CODE } from '../../../../../../../src/plugins/es_ui_shared/static/forms/helpers/field_validators/types';
@ -19,13 +18,13 @@ import { ERROR_CODE } from '../../../../../../../src/plugins/es_ui_shared/static
/**
* Formats the entry into one that is easily usable for the UI.
*
* @param patterns IndexPattern containing available fields on rule index
* @param patterns DataViewBase containing available fields on rule index
* @param item item entry
* @param itemIndex entry index
*/
export const getFormattedEntry = (
indexPattern: IndexPattern,
threatIndexPatterns: IndexPattern,
indexPattern: DataViewBase,
threatIndexPatterns: DataViewBase,
item: Entry,
itemIndex: number,
uuidGen: () => string = uuid.v4
@ -51,12 +50,12 @@ export const getFormattedEntry = (
/**
* Formats the entries to be easily usable for the UI
*
* @param patterns IndexPattern containing available fields on rule index
* @param patterns DataViewBase containing available fields on rule index
* @param entries item entries
*/
export const getFormattedEntries = (
indexPattern: IndexPattern,
threatIndexPatterns: IndexPattern,
indexPattern: DataViewBase,
threatIndexPatterns: DataViewBase,
entries: Entry[]
): FormattedEntry[] => {
return entries.reduce<FormattedEntry[]>((acc, item, index) => {
@ -91,7 +90,7 @@ export const getUpdatedEntriesOnDelete = (
*/
export const getEntryOnFieldChange = (
item: FormattedEntry,
newField: IndexPatternFieldBase
newField: DataViewFieldBase
): { updatedEntry: Entry; index: number } => {
const { entryIndex } = item;
return {
@ -114,7 +113,7 @@ export const getEntryOnFieldChange = (
*/
export const getEntryOnThreatFieldChange = (
item: FormattedEntry,
newField: IndexPatternFieldBase
newField: DataViewFieldBase
): { updatedEntry: Entry; index: number } => {
const { entryIndex } = item;
return {

View file

@ -16,7 +16,7 @@ import { useKibana } from '../../../common/lib/kibana';
import { ThreatMatchComponent } from './';
import { ThreatMapEntries } from './types';
import { IndexPattern } from 'src/plugins/data/public';
import { DataViewBase } from '@kbn/es-query';
import { getMockTheme } from '../../lib/kibana/kibana_react.mock';
const mockTheme = getMockTheme({
@ -65,14 +65,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -94,14 +94,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -123,14 +123,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -151,14 +151,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -188,14 +188,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -225,14 +225,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -255,14 +255,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>
@ -286,14 +286,14 @@ describe('ThreatMatchComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
onChange={jest.fn()}
/>

View file

@ -8,10 +8,9 @@
import React, { useCallback, useEffect, useReducer } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { DataViewBase } from '@kbn/es-query';
import { ThreatMapping } from '@kbn/securitysolution-io-ts-alerting-types';
import { ListItemComponent } from './list_item';
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { AndOrBadge } from '../and_or_badge';
import { LogicButtons } from './logic_buttons';
import { ThreatMapEntries } from './types';
@ -45,8 +44,8 @@ interface OnChangeProps {
interface ThreatMatchComponentProps {
listItems: ThreatMapEntries[];
indexPatterns: IndexPattern;
threatIndexPatterns: IndexPattern;
indexPatterns: DataViewBase;
threatIndexPatterns: DataViewBase;
onChange: (arg: OnChangeProps) => void;
}

View file

@ -14,7 +14,7 @@ import { fields } from '../../../../../../../src/plugins/data/common/mocks';
import { ListItemComponent } from './list_item';
import { ThreatMapEntries } from './types';
import { IndexPattern } from 'src/plugins/data/public';
import { DataViewBase } from '@kbn/es-query';
import { getMockTheme } from '../../lib/kibana/kibana_react.mock';
const mockTheme = getMockTheme({
@ -81,14 +81,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={true}
isOnlyItem={false}
@ -114,7 +114,7 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={true}
isOnlyItem={false}
@ -125,7 +125,7 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
/>
</ThemeProvider>
@ -145,14 +145,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={true}
isOnlyItem={false}
@ -178,14 +178,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={false}
isOnlyItem={false}
@ -219,14 +219,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={false}
isOnlyItem={true}
@ -250,14 +250,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={false}
isOnlyItem={false}
@ -281,14 +281,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={false}
// if entryItemIndex is not 0, wouldn't make sense for
@ -314,14 +314,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={false}
isOnlyItem={true}
@ -346,14 +346,14 @@ describe('ListItemComponent', () => {
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
threatIndexPatterns={
{
id: '1234',
title: 'logstash-*',
fields,
} as IndexPattern
} as DataViewBase
}
andLogicIncluded={false}
isOnlyItem={true}

View file

@ -9,7 +9,7 @@ import React, { useMemo, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import styled from 'styled-components';
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { DataViewBase } from '@kbn/es-query';
import { getFormattedEntries, getUpdatedEntriesOnDelete } from './helpers';
import { FormattedEntry, ThreatMapEntries, Entry } from './types';
import { EntryItem } from './entry_item';
@ -24,8 +24,8 @@ const MyOverflowContainer = styled(EuiFlexItem)`
interface ListItemProps {
listItem: ThreatMapEntries;
listItemIndex: number;
indexPattern: IndexPattern;
threatIndexPatterns: IndexPattern;
indexPattern: DataViewBase;
threatIndexPatterns: DataViewBase;
andLogicIncluded: boolean;
isOnlyItem: boolean;
onDeleteEntryItem: (item: ThreatMapEntries, index: number) => void;

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { IndexPatternFieldBase } from '@kbn/es-query';
import { DataViewFieldBase } from '@kbn/es-query';
import { ThreatMap, ThreatMapEntry } from '@kbn/securitysolution-io-ts-alerting-types';
export interface FormattedEntry {
id: string;
field: IndexPatternFieldBase | undefined;
field: DataViewFieldBase | undefined;
type: 'mapping';
value: IndexPatternFieldBase | undefined;
value: DataViewFieldBase | undefined;
entryIndex: number;
}

View file

@ -8,15 +8,11 @@
import React, { useMemo } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataViewBase, Filter, Query } from '@kbn/es-query';
import { useGlobalTime } from '../../containers/use_global_time';
import { BrowserFields } from '../../containers/source';
import { useKibana } from '../../lib/kibana';
import {
esQuery,
Filter,
Query,
IIndexPattern,
} from '../../../../../../../src/plugins/data/public';
import { esQuery } from '../../../../../../../src/plugins/data/public';
import { inputsModel, inputsSelectors, State } from '../../store';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { timelineSelectors } from '../../../timelines/store/timeline';
@ -77,7 +73,7 @@ const connector = connect(makeMapStateToProps);
export interface OwnProps {
browserFields: BrowserFields;
field: string;
indexPattern: IIndexPattern;
indexPattern: DataViewBase;
timelineId?: string;
toggleTopN: () => void;
onFilterAdded?: () => void;

View file

@ -7,6 +7,7 @@
import { State } from '../../store';
import { sourcererSelectors } from '../../store/selectors';
import { SourcererScopeName } from '../../store/sourcerer/model';
export interface IndicesSelector {
all: string[];
@ -14,25 +15,17 @@ export interface IndicesSelector {
}
export const getIndicesSelector = () => {
const getkibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector();
const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector();
const getSignalIndexNameSelector = sourcererSelectors.signalIndexNameSelector();
const getScopeSelector = sourcererSelectors.scopeIdSelector();
const mapStateToProps = (state: State): IndicesSelector => {
const rawIndices = new Set(getConfigIndexPatternsSelector(state));
const kibanaIndexPatterns = getkibanaIndexPatternsSelector(state);
const alertIndexName = getSignalIndexNameSelector(state);
kibanaIndexPatterns.forEach(({ title }) => {
if (title !== alertIndexName) {
rawIndices.add(title);
}
});
return (state: State, scopeId: SourcererScopeName): IndicesSelector => {
const signalIndexName = getSignalIndexNameSelector(state);
const { selectedPatterns } = getScopeSelector(state, scopeId);
const raw: string[] = selectedPatterns.filter((index) => index !== signalIndexName);
return {
all: alertIndexName != null ? [...rawIndices, alertIndexName] : [...rawIndices],
raw: [...rawIndices],
all: signalIndexName != null ? [...raw, signalIndexName] : [...raw],
raw,
};
};
return mapStateToProps;
};

View file

@ -11,10 +11,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { DataViewBase, Filter, Query } from '@kbn/es-query';
import { GlobalTimeArgs } from '../../containers/use_global_time';
import { EventsByDataset } from '../../../overview/components/events_by_dataset';
import { SignalsByCategory } from '../../../overview/components/signals_by_category';
import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public';
import { InputsModelId } from '../../store/inputs/constants';
import { TimelineEventsType } from '../../../../common/types/timeline';
@ -23,6 +23,7 @@ import * as i18n from './translations';
import { getIndicesSelector, IndicesSelector } from './selectors';
import { State } from '../../store';
import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types';
import { SourcererScopeName } from '../../store/sourcerer/model';
const TopNContainer = styled.div`
min-width: 600px;
@ -53,7 +54,7 @@ export interface Props extends Pick<GlobalTimeArgs, 'from' | 'to' | 'deleteQuery
defaultView: TimelineEventsType;
field: AlertsStackByField;
filters: Filter[];
indexPattern: IIndexPattern;
indexPattern: DataViewBase;
options: TopNOption[];
paddingSize?: 's' | 'm' | 'l' | 'none';
query: Query;
@ -90,7 +91,15 @@ const TopNComponent: React.FC<Props> = ({
);
const indicesSelector = useMemo(getIndicesSelector, []);
const { all: allIndices, raw: rawIndices } = useSelector<State, IndicesSelector>(
(state) => indicesSelector(state),
(state) =>
indicesSelector(
state,
timelineId != null
? defaultView === 'alert'
? SourcererScopeName.detections
: SourcererScopeName.timeline
: SourcererScopeName.default
),
deepEqual
);

View file

@ -24,7 +24,7 @@ import { NavTab } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { ReplaceStateInLocation, KeyUrlState, ValueUrlState } from './types';
import { sourcererSelectors } from '../../store/sourcerer';
import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model';
import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model';
export const isDetectionsPages = (pageName: string) =>
pageName === SecurityPageName.alerts ||
@ -156,9 +156,18 @@ export const makeMapStateToProps = () => {
}
const sourcerer = getSourcererScopes(state);
const activeScopes: SourcererScopeName[] = Object.keys(sourcerer) as SourcererScopeName[];
const selectedPatterns: SourcererScopePatterns = activeScopes
const selectedPatterns: SourcererUrlState = activeScopes
.filter((scope) => scope === SourcererScopeName.default)
.reduce((acc, scope) => ({ ...acc, [scope]: sourcerer[scope]?.selectedPatterns }), {});
.reduce(
(acc, scope) => ({
...acc,
[scope]: {
id: sourcerer[scope]?.selectedDataViewId,
selectedPatterns: sourcerer[scope]?.selectedPatterns,
},
}),
{}
);
return {
urlState: {

View file

@ -28,7 +28,7 @@ import {
queryTimelineById,
dispatchUpdateTimeline,
} from '../../../timelines/components/open_timeline/helpers';
import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model';
import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model';
import { timelineActions } from '../../../timelines/store/timeline';
export const useSetInitialStateFromUrl = () => {
@ -55,16 +55,17 @@ export const useSetInitialStateFromUrl = () => {
updateTimerange(newUrlStateString, dispatch);
}
if (urlKey === CONSTANTS.sourcerer) {
const sourcererState = decodeRisonUrlState<SourcererScopePatterns>(newUrlStateString);
const sourcererState = decodeRisonUrlState<SourcererUrlState>(newUrlStateString);
if (sourcererState != null) {
const activeScopes: SourcererScopeName[] = Object.keys(sourcererState).filter(
(key) => !(key === SourcererScopeName.default && isDetectionsPages(pageName))
) as SourcererScopeName[];
activeScopes.forEach((scope) =>
dispatch(
sourcererActions.setSelectedIndexPatterns({
sourcererActions.setSelectedDataView({
id: scope,
selectedPatterns: sourcererState[scope] ?? [],
selectedDataViewId: sourcererState[scope]?.id ?? '',
selectedPatterns: sourcererState[scope]?.selectedPatterns ?? [],
})
)
);

View file

@ -13,23 +13,12 @@ import {
RelativeTimeRange,
isRelativeTimeRange,
} from '../../store/inputs/model';
import DateMath from '@elastic/datemath';
import { getTimeRangeSettings } from '../../utils/default_date_settings';
const getTimeRangeSettingsMock = getTimeRangeSettings as jest.Mock;
jest.mock('../../utils/default_date_settings');
jest.mock('@elastic/datemath', () => ({
parse: (date: string) => {
if (date === 'now') {
return { toISOString: () => '2020-07-08T08:20:18.966Z' };
}
if (date === 'now-24h') {
return { toISOString: () => '2020-07-07T08:20:18.966Z' };
}
},
}));
getTimeRangeSettingsMock.mockImplementation(() => ({
from: '2020-07-04T08:20:18.966Z',
@ -39,6 +28,19 @@ getTimeRangeSettingsMock.mockImplementation(() => ({
}));
describe('#normalizeTimeRange', () => {
let dateMathSpy: jest.SpyInstance;
beforeAll(() => {
dateMathSpy = jest.spyOn(DateMath, 'parse');
dateMathSpy.mockImplementation((date: string) =>
date === 'now'
? { toISOString: () => new Date('2020-07-08T08:20:18.966Z') }
: { toISOString: () => new Date('2020-07-07T08:20:18.966Z') }
);
});
afterAll(() => {
jest.clearAllMocks();
});
test('Absolute time range returns defaults for empty strings', () => {
const dateTimeRange: URLTimeRange = {
kind: 'absolute',

View file

@ -84,9 +84,7 @@ export const defaultProps: UrlStateContainerPropTypes = {
indexPattern: {
fields: [
{
aggregatable: true,
name: '@timestamp',
searchable: true,
type: 'date',
},
],

View file

@ -5,21 +5,15 @@
* 2.0.
*/
import {
Filter,
FilterManager,
IIndexPattern,
Query,
SavedQueryService,
} from 'src/plugins/data/public';
import { Filter, FilterManager, Query, SavedQueryService } from 'src/plugins/data/public';
import { DataViewBase } from '@kbn/es-query';
import { UrlInputsModel } from '../../store/inputs/model';
import { TimelineUrl } from '../../../timelines/store/timeline/model';
import { RouteSpyState } from '../../utils/route/types';
import { SecurityNav } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { SourcererScopePatterns } from '../../store/sourcerer/model';
import { SourcererUrlState } from '../../store/sourcerer/model';
export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
CONSTANTS.appQuery,
@ -48,7 +42,7 @@ export interface UrlState {
[CONSTANTS.appQuery]?: Query;
[CONSTANTS.filters]?: Filter[];
[CONSTANTS.savedQuery]?: string;
[CONSTANTS.sourcerer]: SourcererScopePatterns;
[CONSTANTS.sourcerer]: SourcererUrlState;
[CONSTANTS.timerange]: UrlInputsModel;
[CONSTANTS.timeline]: TimelineUrl;
}
@ -58,7 +52,7 @@ export type ValueUrlState = UrlState[keyof UrlState];
export interface UrlStateProps {
navTabs: SecurityNav;
indexPattern?: IIndexPattern;
indexPattern?: DataViewBase;
mapToUrlState?: (value: string) => UrlState;
onChange?: (urlState: UrlState, previousUrlState: UrlState) => void;
onInitialize?: (urlState: UrlState) => void;
@ -89,7 +83,7 @@ export interface UrlStateToRedux {
export interface SetInitialStateFromUrl {
filterManager: FilterManager;
indexPattern: IIndexPattern | undefined;
indexPattern: DataViewBase | undefined;
pageName: string;
savedQueries: SavedQueryService;
urlStateToUpdate: UrlStateToRedux[];

View file

@ -1,85 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { QuerySuggestion, IIndexPattern } from '../../../../../../../src/plugins/data/public';
import { useKibana } from '../../lib/kibana';
type RendererResult = React.ReactElement<JSX.Element> | null;
type RendererFunction<RenderArgs, Result = RendererResult> = (args: RenderArgs) => Result;
interface KueryAutocompletionLifecycleProps {
children: RendererFunction<{
isLoadingSuggestions: boolean;
loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void;
suggestions: QuerySuggestion[];
}>;
indexPattern: IIndexPattern;
}
interface KueryAutocompletionCurrentRequest {
expression: string;
cursorPosition: number;
}
export const KueryAutocompletion = React.memo<KueryAutocompletionLifecycleProps>(
({ children, indexPattern }) => {
const [currentRequest, setCurrentRequest] = useState<KueryAutocompletionCurrentRequest | null>(
null
);
const [suggestions, setSuggestions] = useState<QuerySuggestion[]>([]);
const kibana = useKibana();
const loadSuggestions = async (
expression: string,
cursorPosition: number,
maxSuggestions?: number
) => {
const language = 'kuery';
if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) {
return;
}
const futureRequest = {
expression,
cursorPosition,
};
setCurrentRequest({
expression,
cursorPosition,
});
setSuggestions([]);
if (
futureRequest &&
futureRequest.expression !== (currentRequest && currentRequest.expression) &&
futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition)
) {
const newSuggestions =
(await kibana.services.data.autocomplete.getQuerySuggestions({
language: 'kuery',
indexPatterns: [indexPattern],
boolFilter: [],
query: expression,
selectionStart: cursorPosition,
selectionEnd: cursorPosition,
})) || [];
setCurrentRequest(null);
setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions);
}
};
return children({
isLoadingSuggestions: currentRequest !== null,
loadSuggestions,
suggestions,
});
}
);
KueryAutocompletion.displayName = 'KueryAutocompletion';

View file

@ -7,7 +7,31 @@
import { IndexField } from '../../../../common/search_strategy/index_fields';
import { getBrowserFields } from '.';
import { useDataView } from './use_data_view';
import { mockBrowserFields, mocksSource } from './mock';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { createStore, State } from '../../store';
import {
createSecuritySolutionStorageMock,
kibanaObservable,
mockGlobalState,
SUB_PLUGINS_REDUCER,
} from '../../mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { Provider } from 'react-redux';
import React from 'react';
import { useKibana } from '../../lib/kibana';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../lib/kibana'); // , () => ({
describe('source/index.tsx', () => {
describe('getBrowserFields', () => {
@ -28,4 +52,89 @@ describe('source/index.tsx', () => {
expect(fields).toEqual(mockBrowserFields);
});
});
describe('useDataView hook', () => {
const sourcererState = mockGlobalState.sourcerer;
const state: State = {
...mockGlobalState,
sourcerer: {
...sourcererState,
kibanaDataViews: [
...sourcererState.kibanaDataViews,
{
...sourcererState.defaultDataView,
id: 'something-random',
title: 'something,random',
patternList: ['something', 'random'],
},
],
sourcererScopes: {
...sourcererState.sourcererScopes,
[SourcererScopeName.default]: {
...sourcererState.sourcererScopes[SourcererScopeName.default],
},
[SourcererScopeName.detections]: {
...sourcererState.sourcererScopes[SourcererScopeName.detections],
},
[SourcererScopeName.timeline]: {
...sourcererState.sourcererScopes[SourcererScopeName.timeline],
},
},
},
};
const mockSearchResponse = {
...mocksSource,
indicesExist: ['auditbeat-*', sourcererState.signalIndexName],
isRestore: false,
rawResponse: {},
runtimeMappings: {},
};
const { storage } = createSecuritySolutionStorageMock();
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
beforeEach(() => {
jest.clearAllMocks();
const mock = {
subscribe: ({ next }: { next: Function }) => next(mockSearchResponse),
unsubscribe: jest.fn(),
};
(useKibana as jest.Mock).mockReturnValue({
services: {
data: {
search: {
search: jest.fn().mockReturnValue({
subscribe: ({ next }: { next: Function }) => {
next(mockSearchResponse);
return mock;
},
unsubscribe: jest.fn(),
}),
},
},
},
});
});
it('sets field data for data view', async () => {
await act(async () => {
const { rerender, waitForNextUpdate, result } = renderHook<
string,
{ indexFieldsSearch: (id: string) => void }
>(() => useDataView(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
act(() => result.current.indexFieldsSearch('neato'));
expect(mockDispatch.mock.calls[0][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
payload: { id: 'neato', loading: true },
});
const { type: sourceType, payload } = mockDispatch.mock.calls[1][0];
expect(sourceType).toEqual('x-pack/security_solution/local/sourcerer/SET_DATA_VIEW');
expect(payload.id).toEqual('neato');
expect(Object.keys(payload.browserFields)).toHaveLength(10);
expect(payload.docValueFields).toEqual([{ field: '@timestamp' }]);
});
});
});
});

View file

@ -5,27 +5,23 @@
* 2.0.
*/
import { keyBy, pick, isEmpty, isEqual, isUndefined } from 'lodash/fp';
import { isEmpty, isEqual, isUndefined, keyBy, pick } from 'lodash/fp';
import memoizeOne from 'memoize-one';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { IIndexPattern } from 'src/plugins/data/public';
import { useCallback, useEffect, useRef, useState } from 'react';
import { DataViewBase } from '@kbn/es-query';
import { Subscription } from 'rxjs';
import { useKibana } from '../../lib/kibana';
import {
IndexField,
IndexFieldsStrategyResponse,
IndexFieldsStrategyRequest,
BrowserField,
BrowserFields,
} from '../../../../common/search_strategy/index_fields';
import { isErrorResponse, isCompleteResponse } from '../../../../../../../src/plugins/data/common';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
DocValueFields,
IndexField,
IndexFieldsStrategyRequest,
IndexFieldsStrategyResponse,
} from '../../../../../timelines/common';
import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common';
import * as i18n from './translations';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
import { DocValueFields } from '../../../../common/search_strategy/common';
import { useAppToasts } from '../../hooks/use_app_toasts';
export { BrowserField, BrowserFields, DocValueFields };
@ -45,7 +41,7 @@ export const getAllFieldsByName = (
keyBy('name', getAllBrowserFields(browserFields));
export const getIndexFields = memoizeOne(
(title: string, fields: IndexField[]): IIndexPattern =>
(title: string, fields: IndexField[]): DataViewBase =>
fields && fields.length > 0
? {
fields: fields.map((field) =>
@ -95,7 +91,6 @@ export const getDocValueFields = memoizeOne(
...accumulator,
{
field: field.name,
format: field.format ? field.format : undefined,
},
];
}
@ -119,9 +114,13 @@ interface FetchIndexReturn {
docValueFields: DocValueFields[];
indexes: string[];
indexExists: boolean;
indexPatterns: IIndexPattern;
indexPatterns: DataViewBase;
}
/**
* Independent index fields hook/request
* returns state directly, no redux
*/
export const useFetchIndex = (
indexNames: string[],
onlyCheckIfIndicesExist: boolean = false
@ -147,7 +146,7 @@ export const useFetchIndex = (
abortCtrl.current = new AbortController();
setLoading(true);
searchSubscription$.current = data.search
.search<IndexFieldsStrategyRequest, IndexFieldsStrategyResponse>(
.search<IndexFieldsStrategyRequest<'indices'>, IndexFieldsStrategyResponse>(
{ indices: iNames, onlyCheckIfIndicesExist },
{
abortSignal: abortCtrl.current.signal,
@ -161,7 +160,6 @@ export const useFetchIndex = (
previousIndexesName.current = response.indicesExist;
setLoading(false);
setState({
browserFields: getBrowserFields(stringifyIndices, response.indexFields),
docValueFields: getDocValueFields(stringifyIndices, response.indexFields),
@ -205,95 +203,3 @@ export const useFetchIndex = (
return [isLoading, state];
};
export const useIndexFields = (sourcererScopeName: SourcererScopeName) => {
const { data } = useKibana().services;
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const dispatch = useDispatch();
const indexNamesSelectedSelector = useMemo(
() => sourcererSelectors.getIndexNamesSelectedSelector(),
[]
);
const { indexNames, previousIndexNames } = useDeepEqualSelector<{
indexNames: string[];
previousIndexNames: string;
}>((state) => indexNamesSelectedSelector(state, sourcererScopeName));
const { addError, addWarning } = useAppToasts();
const setLoading = useCallback(
(loading: boolean) => {
dispatch(sourcererActions.setSourcererScopeLoading({ id: sourcererScopeName, loading }));
},
[dispatch, sourcererScopeName]
);
const indexFieldsSearch = useCallback(
(indicesName) => {
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);
searchSubscription$.current = data.search
.search<IndexFieldsStrategyRequest, IndexFieldsStrategyResponse>(
{ indices: indicesName, onlyCheckIfIndicesExist: false },
{
abortSignal: abortCtrl.current.signal,
strategy: 'indexFields',
}
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
const stringifyIndices = response.indicesExist.sort().join();
dispatch(
sourcererActions.setSource({
id: sourcererScopeName,
payload: {
browserFields: getBrowserFields(stringifyIndices, response.indexFields),
docValueFields: getDocValueFields(stringifyIndices, response.indexFields),
errorMessage: null,
id: sourcererScopeName,
indexPattern: getIndexFields(stringifyIndices, response.indexFields),
// If checking for DE signals index, lie and say the index is created (it's
// no longer created on startup, but is created lazily before writing).
indicesExist:
sourcererScopeName === SourcererScopeName.detections
? true
: response.indicesExist.length > 0,
loading: false,
},
})
);
searchSubscription$.current.unsubscribe();
} else if (isErrorResponse(response)) {
setLoading(false);
addWarning(i18n.ERROR_BEAT_FIELDS);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: i18n.FAIL_BEAT_FIELDS,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
},
[data.search, dispatch, addError, addWarning, setLoading, sourcererScopeName]
);
useEffect(() => {
if (!isEmpty(indexNames) && previousIndexNames !== indexNames.sort().join()) {
indexFieldsSearch(indexNames);
}
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [indexNames, indexFieldsSearch, previousIndexNames]);
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants';
import { DocValueFields } from '../../../../common/search_strategy';
import { BrowserFields } from '../../../../common/search_strategy/index_fields';
@ -22,6 +23,7 @@ export const mocksSource = {
searchable: true,
type: 'date',
aggregatable: true,
readFromDocValues: true,
},
{
category: 'agent',
@ -330,7 +332,13 @@ export const mocksSource = {
};
export const mockIndexFields = [
{ aggregatable: true, name: '@timestamp', searchable: true, type: 'date' },
{
aggregatable: true,
name: '@timestamp',
searchable: true,
type: 'date',
readFromDocValues: true,
},
{ aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' },
{ aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' },
{ aggregatable: true, name: 'agent.id', searchable: true, type: 'string' },
@ -459,6 +467,7 @@ export const mockBrowserFields: BrowserFields = {
name: '@timestamp',
searchable: true,
type: 'date',
readFromDocValues: true,
},
},
},
@ -726,3 +735,12 @@ export const mockDocValueFields: DocValueFields[] = [
format: 'date_time',
},
];
export const mockRuntimeMappings: MappingRuntimeFields = {
'@a.runtime.field': {
script: {
source: 'emit("Radical dude: " + doc[\'host.name\'].value)',
},
type: 'keyword',
},
};

View file

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useEffect, useRef } from 'react';
import { Subscription } from 'rxjs';
import { useDispatch } from 'react-redux';
import memoizeOne from 'memoize-one';
import { pick } from 'lodash/fp';
import { useKibana } from '../../lib/kibana';
import { useAppToasts } from '../../hooks/use_app_toasts';
import { sourcererActions } from '../../store/sourcerer';
import {
DELETED_SECURITY_SOLUTION_DATA_VIEW,
IndexField,
IndexFieldsStrategyRequest,
IndexFieldsStrategyResponse,
} from '../../../../../timelines/common';
import {
FieldSpec,
isCompleteResponse,
isErrorResponse,
} from '../../../../../../../src/plugins/data/common';
import * as i18n from './translations';
import { getBrowserFields, getDocValueFields } from './';
const getEsFields = memoizeOne(
(fields: IndexField[]): FieldSpec[] =>
fields && fields.length > 0
? fields.map((field) =>
pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field)
)
: [],
(newArgs, lastArgs) => newArgs[0].length === lastArgs[0].length
);
export const useDataView = (): { indexFieldsSearch: (selectedDataViewId: string) => void } => {
const { data } = useKibana().services;
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const dispatch = useDispatch();
const { addError, addWarning } = useAppToasts();
const setLoading = useCallback(
({ id, loading }: { id: string; loading: boolean }) => {
dispatch(sourcererActions.setDataViewLoading({ id, loading }));
},
[dispatch]
);
const indexFieldsSearch = useCallback(
(selectedDataViewId: string) => {
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading({ id: selectedDataViewId, loading: true });
searchSubscription$.current = data.search
.search<IndexFieldsStrategyRequest<'dataView'>, IndexFieldsStrategyResponse>(
{
dataViewId: selectedDataViewId,
onlyCheckIfIndicesExist: false,
},
{
abortSignal: abortCtrl.current.signal,
strategy: 'indexFields',
}
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
const patternString = response.indicesExist.sort().join();
dispatch(
sourcererActions.setDataView({
browserFields: getBrowserFields(patternString, response.indexFields),
docValueFields: getDocValueFields(patternString, response.indexFields),
id: selectedDataViewId,
indexFields: getEsFields(response.indexFields),
loading: false,
runtimeMappings: response.runtimeMappings,
})
);
searchSubscription$.current.unsubscribe();
} else if (isErrorResponse(response)) {
setLoading({ id: selectedDataViewId, loading: false });
addWarning(i18n.ERROR_BEAT_FIELDS);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
if (msg.message === DELETED_SECURITY_SOLUTION_DATA_VIEW) {
// reload app if security solution data view is deleted
return location.reload();
}
setLoading({ id: selectedDataViewId, loading: false });
addError(msg, {
title: i18n.FAIL_BEAT_FIELDS,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
},
[addError, addWarning, data.search, dispatch, setLoading]
);
useEffect(() => {
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, []);
return { indexFieldsSearch };
};

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { KibanaServices } from '../../lib/kibana';
import { SOURCERER_API_URL } from '../../../../common/constants';
import { KibanaDataView } from '../../store/sourcerer/model';
export interface GetSourcererDataView {
signal: AbortSignal;
body: {
patternList: string[];
};
}
export interface SecurityDataView {
defaultDataView: KibanaDataView;
kibanaDataViews: KibanaDataView[];
}
export const postSourcererDataView = async ({
body,
signal,
}: GetSourcererDataView): Promise<SecurityDataView> =>
KibanaServices.get().http.fetch(SOURCERER_API_URL, {
method: 'POST',
body: JSON.stringify(body),
signal,
});

View file

@ -7,14 +7,14 @@
import React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { waitFor } from '@testing-library/react';
import { Provider } from 'react-redux';
import { getScopeFromPath, useInitSourcerer } from '.';
import { getScopeFromPath, useInitSourcerer, useSourcererDataView } from '.';
import { mockPatterns } from './mocks';
// import { SourcererScopeName } from '../../store/sourcerer/model';
import { RouteSpyState } from '../../utils/route/types';
import { SecurityPageName } from '../../../../common/constants';
import { createStore, State } from '../../store';
import { DEFAULT_INDEX_PATTERN, SecurityPageName } from '../../../../common/constants';
import { createStore } from '../../store';
import {
useUserInfo,
initialState as userInfoState,
@ -24,8 +24,10 @@ import {
kibanaObservable,
mockGlobalState,
SUB_PLUGINS_REDUCER,
mockSourcererState,
} from '../../mock';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model';
import { postSourcererDataView } from './api';
const mockRouteSpy: RouteSpyState = {
pageName: SecurityPageName.overview,
@ -37,6 +39,7 @@ const mockRouteSpy: RouteSpyState = {
const mockDispatch = jest.fn();
const mockUseUserInfo = useUserInfo as jest.Mock;
jest.mock('../../../detections/components/user_info');
jest.mock('./api');
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
@ -48,6 +51,7 @@ jest.mock('react-redux', () => {
jest.mock('../../utils/route/use_route_spy', () => ({
useRouteSpy: () => [mockRouteSpy],
}));
jest.mock('../../lib/kibana', () => ({
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
@ -84,36 +88,13 @@ jest.mock('../../lib/kibana', () => ({
}));
describe('Sourcerer Hooks', () => {
const state: State = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
indexPattern: {
fields: [],
title: '',
},
},
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
indexPattern: {
fields: [],
title: '',
},
},
},
},
};
const { storage } = createSecuritySolutionStorageMock();
let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
let store: ReturnType<typeof createStore>;
beforeEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
mockUseUserInfo.mockImplementation(() => userInfoState);
});
it('initializes loading default and timeline index patterns', async () => {
@ -125,34 +106,88 @@ describe('Sourcerer Hooks', () => {
rerender();
expect(mockDispatch).toBeCalledTimes(2);
expect(mockDispatch.mock.calls[0][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
payload: { id: 'default', loading: true },
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
payload: { id: 'security-solution', loading: true },
});
expect(mockDispatch.mock.calls[1][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
payload: { id: 'timeline', loading: true },
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
payload: {
id: 'timeline',
selectedDataViewId: 'security-solution',
selectedPatterns: [
'.siem-signals-spacename',
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
],
},
});
});
});
it('sets signal index name', async () => {
const mockNewDataViews = {
defaultDataView: mockSourcererState.defaultDataView,
kibanaDataViews: [mockSourcererState.defaultDataView],
};
(postSourcererDataView as jest.Mock).mockResolvedValue(mockNewDataViews);
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
signalIndexName: null,
defaultDataView: {
...mockGlobalState.sourcerer.defaultDataView,
title: DEFAULT_INDEX_PATTERN.join(','),
patternList: DEFAULT_INDEX_PATTERN,
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
await act(async () => {
mockUseUserInfo.mockImplementation(() => ({
...userInfoState,
loading: false,
signalIndexName: 'signals-*',
signalIndexName: mockSourcererState.signalIndexName,
}));
const { rerender, waitForNextUpdate } = renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
expect(mockDispatch.mock.calls[2][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SIGNAL_INDEX_NAME',
payload: { signalIndexName: 'signals-*' },
});
expect(mockDispatch.mock.calls[3][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_INDEX_PATTERNS',
payload: { id: 'timeline', selectedPatterns: ['signals-*'] },
await waitFor(() => {
expect(mockDispatch.mock.calls[2][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
payload: { loading: true },
});
expect(mockDispatch.mock.calls[3][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SIGNAL_INDEX_NAME',
payload: { signalIndexName: mockSourcererState.signalIndexName },
});
expect(mockDispatch.mock.calls[4][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_DATA_VIEW_LOADING',
payload: {
id: mockSourcererState.defaultDataView.id,
loading: true,
},
});
expect(mockDispatch.mock.calls[5][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_DATA_VIEWS',
payload: mockNewDataViews,
});
expect(mockDispatch.mock.calls[6][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING',
payload: { loading: false },
});
});
});
});
@ -160,7 +195,7 @@ describe('Sourcerer Hooks', () => {
await act(async () => {
mockUseUserInfo.mockImplementation(() => ({
...userInfoState,
signalIndexName: 'signals-*',
signalIndexName: mockSourcererState.signalIndexName,
isSignalIndexExists: true,
}));
const { rerender, waitForNextUpdate } = renderHook<string, void>(
@ -171,9 +206,130 @@ describe('Sourcerer Hooks', () => {
);
await waitForNextUpdate();
rerender();
expect(mockDispatch.mock.calls[1][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_INDEX_PATTERNS',
payload: { id: 'detections', selectedPatterns: ['signals-*'] },
expect(mockDispatch.mock.calls[2][0]).toEqual({
type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW',
payload: {
id: 'detections',
selectedDataViewId: mockSourcererState.defaultDataView.id,
selectedPatterns: [mockSourcererState.signalIndexName],
},
});
});
});
describe('useSourcererDataView', () => {
it('Should exclude elastic cloud alias when selected patterns include "logs-*" as an alias', async () => {
await act(async () => {
const { result, rerender, waitForNextUpdate } = renderHook<
SourcererScopeName,
SelectedDataView
>(() => useSourcererDataView(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
expect(result.current.selectedPatterns).toEqual([
'-*elastic-cloud-logs-*',
...mockGlobalState.sourcerer.sourcererScopes.default.selectedPatterns,
]);
});
});
it('Should NOT exclude elastic cloud alias when selected patterns does NOT include "logs-*" as an alias', async () => {
await act(async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedPatterns: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const { result, rerender, waitForNextUpdate } = renderHook<
SourcererScopeName,
SelectedDataView
>(() => useSourcererDataView(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
expect(result.current.selectedPatterns).toEqual([
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
]);
});
});
it('Should NOT exclude elastic cloud alias when selected patterns include "logs-endpoint.event-*" as an alias', async () => {
await act(async () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedPatterns: [
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
'logs-endpoint.event-*',
],
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const { result, rerender, waitForNextUpdate } = renderHook<
SourcererScopeName,
SelectedDataView
>(() => useSourcererDataView(), {
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
});
await waitForNextUpdate();
rerender();
expect(result.current.selectedPatterns).toEqual([
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-endpoint.event-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
]);
});
});
});

View file

@ -5,133 +5,298 @@
* 2.0.
*/
import { useEffect, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { matchPath } from 'react-router-dom';
import { sourcererActions, sourcererSelectors } from '../../store/sourcerer';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useIndexFields } from '../source';
import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model';
import { useUserInfo } from '../../../detections/components/user_info';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { ALERTS_PATH, CASES_PATH, RULES_PATH, UEBA_PATH } from '../../../../common/constants';
import { TimelineId } from '../../../../common';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { getScopePatternListSelection } from '../../store/sourcerer/helpers';
import { useAppToasts } from '../../hooks/use_app_toasts';
import { postSourcererDataView } from './api';
import { useDataView } from '../source/use_data_view';
export const useInitSourcerer = (
scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default
) => {
const dispatch = useDispatch();
const abortCtrl = useRef(new AbortController());
const initialTimelineSourcerer = useRef(true);
const initialDetectionSourcerer = useRef(true);
const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo();
const getConfigIndexPatternsSelector = useMemo(
() => sourcererSelectors.configIndexPatternsSelector(),
const getDefaultDataViewSelector = useMemo(
() => sourcererSelectors.defaultDataViewSelector(),
[]
);
const ConfigIndexPatterns = useDeepEqualSelector(getConfigIndexPatternsSelector);
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
const { addError } = useAppToasts();
useEffect(() => {
if (defaultDataView.error != null) {
addError(defaultDataView.error, {
title: i18n.translate('xpack.securitySolution.sourcerer.permissions.title', {
defaultMessage: 'Write role required to generate data',
}),
toastMessage: i18n.translate('xpack.securitySolution.sourcerer.permissions.toastMessage', {
defaultMessage:
'Users with write permission need to access the Elastic Security app to initialize the app source data.',
}),
});
}
}, [addError, defaultDataView.error]);
const getSignalIndexNameSelector = useMemo(
() => sourcererSelectors.signalIndexNameSelector(),
[]
);
const signalIndexNameSelector = useDeepEqualSelector(getSignalIndexNameSelector);
const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector);
const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const activeTimeline = useDeepEqualSelector((state) =>
getTimelineSelector(state, TimelineId.active)
);
const scopeIdSelector = useMemo(() => sourcererSelectors.scopeIdSelector(), []);
const { selectedDataViewId: scopeDataViewId } = useDeepEqualSelector((state) =>
scopeIdSelector(state, scopeId)
);
const { selectedDataViewId: timelineDataViewId } = useDeepEqualSelector((state) =>
scopeIdSelector(state, SourcererScopeName.timeline)
);
const activeDataViewIds = useMemo(
() => [...new Set([scopeDataViewId, timelineDataViewId])],
[scopeDataViewId, timelineDataViewId]
);
const { indexFieldsSearch } = useDataView();
useIndexFields(scopeId);
useIndexFields(SourcererScopeName.timeline);
useEffect(() => {
if (!loadingSignalIndex && signalIndexName != null && signalIndexNameSelector == null) {
dispatch(sourcererActions.setSignalIndexName({ signalIndexName }));
}
}, [dispatch, loadingSignalIndex, signalIndexName, signalIndexNameSelector]);
useEffect(
() => activeDataViewIds.forEach((id) => id != null && id.length > 0 && indexFieldsSearch(id)),
[activeDataViewIds, indexFieldsSearch]
);
// Related to timeline
useEffect(() => {
if (
!loadingSignalIndex &&
signalIndexName != null &&
signalIndexNameSelector == null &&
signalIndexNameSourcerer == null &&
(activeTimeline == null || activeTimeline.savedObjectId == null) &&
initialTimelineSourcerer.current
initialTimelineSourcerer.current &&
defaultDataView.id.length > 0
) {
initialTimelineSourcerer.current = false;
dispatch(
sourcererActions.setSelectedIndexPatterns({
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedPatterns: [...ConfigIndexPatterns, signalIndexName],
selectedDataViewId: defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.timeline,
signalIndexName,
true
),
})
);
} else if (
signalIndexNameSelector != null &&
signalIndexNameSourcerer != null &&
(activeTimeline == null || activeTimeline.savedObjectId == null) &&
initialTimelineSourcerer.current
initialTimelineSourcerer.current &&
defaultDataView.id.length > 0
) {
initialTimelineSourcerer.current = false;
dispatch(
sourcererActions.setSelectedIndexPatterns({
sourcererActions.setSelectedDataView({
id: SourcererScopeName.timeline,
selectedPatterns: [...ConfigIndexPatterns, signalIndexNameSelector],
selectedDataViewId: defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.timeline,
signalIndexNameSourcerer,
true
),
})
);
}
}, [
activeTimeline,
ConfigIndexPatterns,
defaultDataView,
dispatch,
loadingSignalIndex,
signalIndexName,
signalIndexNameSelector,
signalIndexNameSourcerer,
]);
const updateSourcererDataView = useCallback(
(newSignalsIndex: string) => {
const asyncSearch = async (newPatternList: string[]) => {
abortCtrl.current = new AbortController();
dispatch(sourcererActions.setSourcererScopeLoading({ loading: true }));
try {
const response = await postSourcererDataView({
body: { patternList: newPatternList },
signal: abortCtrl.current.signal,
});
if (response.defaultDataView.patternList.includes(newSignalsIndex)) {
// first time signals is defined and validated in the sourcerer
// redo indexFieldsSearch
indexFieldsSearch(response.defaultDataView.id);
}
dispatch(sourcererActions.setSourcererDataViews(response));
dispatch(sourcererActions.setSourcererScopeLoading({ loading: false }));
} catch (err) {
addError(err, {
title: i18n.translate('xpack.securitySolution.sourcerer.error.title', {
defaultMessage: 'Error updating Security Data View',
}),
toastMessage: i18n.translate('xpack.securitySolution.sourcerer.error.toastMessage', {
defaultMessage: 'Refresh the page',
}),
});
dispatch(sourcererActions.setSourcererScopeLoading({ loading: false }));
}
};
if (defaultDataView.title.indexOf(newSignalsIndex) === -1) {
abortCtrl.current.abort();
asyncSearch([...defaultDataView.title.split(','), newSignalsIndex]);
}
},
[defaultDataView.title, dispatch, indexFieldsSearch, addError]
);
useEffect(() => {
if (
!loadingSignalIndex &&
signalIndexName != null &&
signalIndexNameSourcerer == null &&
defaultDataView.id.length > 0
) {
// update signal name also updates sourcerer
// we hit this the first time signal index is created
updateSourcererDataView(signalIndexName);
dispatch(sourcererActions.setSignalIndexName({ signalIndexName }));
}
}, [
defaultDataView.id,
dispatch,
indexFieldsSearch,
isSignalIndexExists,
loadingSignalIndex,
signalIndexName,
signalIndexNameSourcerer,
updateSourcererDataView,
]);
// Related to the detection page
useEffect(() => {
if (
scopeId === SourcererScopeName.detections &&
isSignalIndexExists &&
signalIndexName != null &&
initialDetectionSourcerer.current
initialDetectionSourcerer.current &&
defaultDataView.id.length > 0
) {
initialDetectionSourcerer.current = false;
dispatch(
sourcererActions.setSelectedIndexPatterns({
id: scopeId,
selectedPatterns: [signalIndexName],
sourcererActions.setSelectedDataView({
id: SourcererScopeName.detections,
selectedDataViewId: defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.detections,
signalIndexName,
true
),
})
);
} else if (
scopeId === SourcererScopeName.detections &&
signalIndexNameSelector != null &&
initialTimelineSourcerer.current
signalIndexNameSourcerer != null &&
initialTimelineSourcerer.current &&
defaultDataView.id.length > 0
) {
initialDetectionSourcerer.current = false;
dispatch(
sourcererActions.setSelectedIndexPatterns({
id: scopeId,
selectedPatterns: [signalIndexNameSelector],
})
);
sourcererActions.setSelectedDataView({
id: SourcererScopeName.detections,
selectedDataViewId: defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.detections,
signalIndexNameSourcerer,
true
),
});
}
}, [dispatch, isSignalIndexExists, scopeId, signalIndexName, signalIndexNameSelector]);
}, [
defaultDataView,
dispatch,
isSignalIndexExists,
scopeId,
signalIndexName,
signalIndexNameSourcerer,
]);
};
export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => {
const LOGS_WILDCARD_INDEX = 'logs-*';
export const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*';
export const useSourcererDataView = (
scopeId: SourcererScopeName = SourcererScopeName.default
): SelectedDataView => {
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
return useDeepEqualSelector((state) => sourcererScopeSelector(state, scope));
const {
signalIndexName,
sourcererDataView: selectedDataView,
sourcererScope: { selectedPatterns: scopeSelectedPatterns, loading },
}: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) =>
sourcererScopeSelector(state, scopeId)
);
const selectedPatterns = useMemo(
() =>
scopeSelectedPatterns.some((index) => index === LOGS_WILDCARD_INDEX)
? [...scopeSelectedPatterns, EXCLUDE_ELASTIC_CLOUD_INDEX]
: scopeSelectedPatterns,
[scopeSelectedPatterns]
);
return useMemo(
() => ({
browserFields: selectedDataView.browserFields,
dataViewId: selectedDataView.id,
docValueFields: selectedDataView.docValueFields,
indexPattern: {
fields: selectedDataView.indexFields,
title: selectedPatterns.join(','),
},
indicesExist:
scopeId === SourcererScopeName.detections
? selectedDataView.patternList.includes(`${signalIndexName}`)
: scopeId === SourcererScopeName.default
? selectedDataView.patternList.filter((i) => i !== signalIndexName).length > 0
: selectedDataView.patternList.length > 0,
loading: loading || selectedDataView.loading,
runtimeMappings: selectedDataView.runtimeMappings,
// all active & inactive patterns in DATA_VIEW
patternList: selectedDataView.title.split(','),
// selected patterns in DATA_VIEW
selectedPatterns: selectedPatterns.sort(),
}),
[loading, selectedPatterns, signalIndexName, scopeId, selectedDataView]
);
};
export const getScopeFromPath = (
pathname: string
): SourcererScopeName.default | SourcererScopeName.detections => {
return matchPath(pathname, {
): SourcererScopeName.default | SourcererScopeName.detections =>
matchPath(pathname, {
path: [ALERTS_PATH, `${RULES_PATH}/id/:id`, `${UEBA_PATH}/:id`, `${CASES_PATH}/:detailName`],
strict: false,
}) == null
? SourcererScopeName.default
: SourcererScopeName.detections;
};

View file

@ -0,0 +1,95 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
createSecuritySolutionStorageMock,
kibanaObservable,
mockGlobalState,
SUB_PLUGINS_REDUCER,
TestProviders,
} from '../../mock';
import { act, renderHook } from '@testing-library/react-hooks';
import { useSignalHelpers } from './use_signal_helpers';
import { createStore, State } from '../../store';
describe('useSignalHelpers', () => {
const wrapperContainer: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
<TestProviders>{children}</TestProviders>
);
test('Default state, does not need init and does not need poll', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), {
wrapper: wrapperContainer,
});
await waitForNextUpdate();
expect(result.current.signalIndexNeedsInit).toEqual(false);
expect(result.current.pollForSignalIndex).toEqual(undefined);
});
});
test('Needs init and does not need poll when signal index is not yet in default data view', async () => {
const state: State = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
defaultDataView: {
...mockGlobalState.sourcerer.defaultDataView,
title: `auditbeat-*,packetbeat-*`,
patternList: ['packetbeat-*', 'auditbeat-*'],
},
kibanaDataViews: [
{
...mockGlobalState.sourcerer.defaultDataView,
title: `auditbeat-*,packetbeat-*`,
patternList: ['packetbeat-*', 'auditbeat-*'],
},
],
},
};
const { storage } = createSecuritySolutionStorageMock();
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), {
wrapper: ({ children }) => <TestProviders store={store}>{children}</TestProviders>,
});
await waitForNextUpdate();
expect(result.current.signalIndexNeedsInit).toEqual(true);
expect(result.current.pollForSignalIndex).toEqual(undefined);
});
});
test('Init happened and signal index does not have data yet, poll function becomes available', async () => {
const state: State = {
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
defaultDataView: {
...mockGlobalState.sourcerer.defaultDataView,
title: `auditbeat-*,packetbeat-*,${mockGlobalState.sourcerer.signalIndexName}`,
patternList: ['packetbeat-*', 'auditbeat-*'],
},
kibanaDataViews: [
{
...mockGlobalState.sourcerer.defaultDataView,
title: `auditbeat-*,packetbeat-*,${mockGlobalState.sourcerer.signalIndexName}`,
patternList: ['packetbeat-*', 'auditbeat-*'],
},
],
},
};
const { storage } = createSecuritySolutionStorageMock();
const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage);
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useSignalHelpers(), {
wrapper: ({ children }) => <TestProviders store={store}>{children}</TestProviders>,
});
await waitForNextUpdate();
expect(result.current.signalIndexNeedsInit).toEqual(false);
expect(result.current.pollForSignalIndex).not.toEqual(undefined);
});
});
});

View file

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { sourcererSelectors } from '../../store';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useSourcererDataView } from '.';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { postSourcererDataView } from './api';
import { sourcererActions } from '../../store/sourcerer';
import { useDataView } from '../source/use_data_view';
import { useAppToasts } from '../../hooks/use_app_toasts';
export const useSignalHelpers = (): {
/* when defined, signal index has been initiated but does not exist */
pollForSignalIndex?: () => void;
/* when false, signal index has been initiated */
signalIndexNeedsInit: boolean;
} => {
const { indicesExist } = useSourcererDataView(SourcererScopeName.detections);
const { indexFieldsSearch } = useDataView();
const dispatch = useDispatch();
const { addError } = useAppToasts();
const abortCtrl = useRef(new AbortController());
const getDefaultDataViewSelector = useMemo(
() => sourcererSelectors.defaultDataViewSelector(),
[]
);
const getSignalIndexNameSelector = useMemo(
() => sourcererSelectors.signalIndexNameSelector(),
[]
);
const signalIndexNameSourcerer = useDeepEqualSelector(getSignalIndexNameSelector);
const defaultDataView = useDeepEqualSelector(getDefaultDataViewSelector);
const signalIndexNeedsInit = useMemo(
() => !defaultDataView.title.includes(`${signalIndexNameSourcerer}`),
[defaultDataView.title, signalIndexNameSourcerer]
);
const shouldWePollForIndex = useMemo(
() => !indicesExist && !signalIndexNeedsInit,
[indicesExist, signalIndexNeedsInit]
);
const pollForSignalIndex = useCallback(() => {
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
try {
const response = await postSourcererDataView({
body: { patternList: defaultDataView.title.split(',') },
signal: abortCtrl.current.signal,
});
if (
signalIndexNameSourcerer !== null &&
response.defaultDataView.patternList.includes(signalIndexNameSourcerer)
) {
// first time signals is defined and validated in the sourcerer
// redo indexFieldsSearch
indexFieldsSearch(response.defaultDataView.id);
dispatch(sourcererActions.setSourcererDataViews(response));
}
} catch (err) {
addError(err, {
title: i18n.translate('xpack.securitySolution.sourcerer.error.title', {
defaultMessage: 'Error updating Security Data View',
}),
toastMessage: i18n.translate('xpack.securitySolution.sourcerer.error.toastMessage', {
defaultMessage: 'Refresh the page',
}),
});
}
};
if (signalIndexNameSourcerer !== null) {
abortCtrl.current.abort();
asyncSearch();
}
}, [addError, defaultDataView.title, dispatch, indexFieldsSearch, signalIndexNameSourcerer]);
return {
...(shouldWePollForIndex ? { pollForSignalIndex } : {}),
signalIndexNeedsInit,
};
};

View file

@ -14,12 +14,12 @@ import {
buildEsQuery,
toElasticsearchQuery,
fromKueryExpression,
IndexPatternBase,
DataViewBase,
} from '@kbn/es-query';
export const convertKueryToElasticSearchQuery = (
kueryExpression: string,
indexPattern?: IndexPatternBase
indexPattern?: DataViewBase
) => {
try {
return kueryExpression
@ -30,10 +30,7 @@ export const convertKueryToElasticSearchQuery = (
}
};
export const convertKueryToDslFilter = (
kueryExpression: string,
indexPattern: IndexPatternBase
) => {
export const convertKueryToDslFilter = (kueryExpression: string, indexPattern: DataViewBase) => {
try {
return kueryExpression
? toElasticsearchQuery(fromKueryExpression(kueryExpression), indexPattern)
@ -75,7 +72,7 @@ export const convertToBuildEsQuery = ({
filters,
}: {
config: EsQueryConfig;
indexPattern: IndexPatternBase;
indexPattern: DataViewBase;
queries: Query[];
filters: Filter[];
}): [string, undefined] | [undefined, Error] => {

View file

@ -26,6 +26,8 @@ import {
DEFAULT_INTERVAL_TYPE,
DEFAULT_INTERVAL_VALUE,
DEFAULT_INDEX_PATTERN,
DEFAULT_DATA_VIEW_ID,
DEFAULT_SIGNALS_INDEX,
} from '../../../common/constants';
import { networkModel } from '../../network/store';
import { uebaModel } from '../../ueba/store';
@ -33,9 +35,30 @@ import { TimelineType, TimelineStatus, TimelineTabs } from '../../../common/type
import { mockManagementState } from '../../management/store/reducer';
import { ManagementState } from '../../management/types';
import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model';
import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock';
import { mockIndexPattern } from './index_pattern';
import { allowedExperimentalValues } from '../../../common/experimental_features';
import { getScopePatternListSelection } from '../store/sourcerer/helpers';
import {
mockBrowserFields,
mockDocValueFields,
mockIndexFields,
mockRuntimeMappings,
} from '../containers/source/mock';
export const mockSourcererState = {
...initialSourcererState,
signalIndexName: `${DEFAULT_SIGNALS_INDEX}-spacename`,
defaultDataView: {
...initialSourcererState.defaultDataView,
browserFields: mockBrowserFields,
docValueFields: mockDocValueFields,
id: DEFAULT_DATA_VIEW_ID,
indexFields: mockIndexFields,
loading: false,
patternList: [...DEFAULT_INDEX_PATTERN, `${DEFAULT_SIGNALS_INDEX}-spacename`],
runtimeMappings: mockRuntimeMappings,
title: [...DEFAULT_INDEX_PATTERN, `${DEFAULT_SIGNALS_INDEX}-spacename`].join(','),
},
};
export const mockGlobalState: State = {
app: {
@ -241,6 +264,7 @@ export const mockGlobalState: State = {
test: {
activeTab: TimelineTabs.query,
prevActiveTab: TimelineTabs.notes,
dataViewId: DEFAULT_DATA_VIEW_ID,
deletedEventIds: [],
documentType: '',
queryFields: [],
@ -294,24 +318,48 @@ export const mockGlobalState: State = {
insertTimeline: null,
},
sourcerer: {
...initialSourcererState,
...mockSourcererState,
defaultDataView: {
...mockSourcererState.defaultDataView,
title: `${mockSourcererState.defaultDataView.title},fakebeat-*`,
},
kibanaDataViews: [
{
...mockSourcererState.defaultDataView,
title: `${mockSourcererState.defaultDataView.title},fakebeat-*`,
},
],
sourcererScopes: {
...initialSourcererState.sourcererScopes,
...mockSourcererState.sourcererScopes,
[SourcererScopeName.default]: {
...initialSourcererState.sourcererScopes[SourcererScopeName.default],
selectedPatterns: DEFAULT_INDEX_PATTERN,
browserFields: mockBrowserFields,
indexPattern: mockIndexPattern,
docValueFields: mockDocValueFields,
loading: false,
...mockSourcererState.sourcererScopes[SourcererScopeName.default],
selectedDataViewId: mockSourcererState.defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
mockSourcererState.defaultDataView,
SourcererScopeName.default,
mockSourcererState.signalIndexName,
true
),
},
[SourcererScopeName.detections]: {
...mockSourcererState.sourcererScopes[SourcererScopeName.detections],
selectedDataViewId: mockSourcererState.defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
mockSourcererState.defaultDataView,
SourcererScopeName.detections,
mockSourcererState.signalIndexName,
true
),
},
[SourcererScopeName.timeline]: {
...initialSourcererState.sourcererScopes[SourcererScopeName.timeline],
selectedPatterns: DEFAULT_INDEX_PATTERN,
browserFields: mockBrowserFields,
indexPattern: mockIndexPattern,
docValueFields: mockDocValueFields,
loading: false,
...mockSourcererState.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: mockSourcererState.defaultDataView.id,
selectedPatterns: getScopePatternListSelection(
mockSourcererState.defaultDataView,
SourcererScopeName.timeline,
mockSourcererState.signalIndexName,
true
),
},
},
},

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { IIndexPattern } from '../../../../../../src/plugins/data/common';
import { SecuritySolutionDataViewBase } from '../types';
export const mockIndexPattern: IIndexPattern = {
export const mockIndexPattern: SecuritySolutionDataViewBase = {
fields: [
{
name: '@timestamp',

View file

@ -1955,6 +1955,7 @@ export const mockTimelineModel: TimelineModel = {
columns: mockTimelineModelColumns,
defaultColumns: mockTimelineModelColumns,
dataProviders: [],
dataViewId: '',
dateRange: {
end: '2020-03-18T13:52:38.929Z',
start: '2020-03-18T13:46:38.929Z',
@ -2091,6 +2092,7 @@ export const defaultTimelineProps: CreateTimelineProps = {
queryMatch: { field: '_id', operator: ':', value: '1' },
},
],
dataViewId: '',
dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' },
deletedEventIds: [],
description: '',

View file

@ -5,10 +5,15 @@
* 2.0.
*/
import { parseExperimentalConfigValue } from '../../..//common/experimental_features';
import { parseExperimentalConfigValue } from '../../../common/experimental_features';
import { SecuritySubPlugins } from '../../app/types';
import { createInitialState } from './reducer';
import { mockSourcererState } from '../mock';
import { useSourcererDataView } from '../containers/sourcerer';
import { useDeepEqualSelector } from '../hooks/use_selector';
import { renderHook } from '@testing-library/react-hooks';
jest.mock('../hooks/use_selector');
jest.mock('../lib/kibana', () => ({
KibanaServices: {
get: jest.fn(() => ({ uiSettings: { get: () => ({ from: 'now-24h', to: 'now' }) } })),
@ -21,26 +26,38 @@ describe('createInitialState', () => {
SecuritySubPlugins['store']['initialState'],
'app' | 'dragAndDrop' | 'inputs' | 'sourcerer'
>;
test('indicesExist should be TRUE if configIndexPatterns is NOT empty', () => {
const initState = createInitialState(mockPluginState, {
kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }],
configIndexPatterns: ['auditbeat-*', 'filebeat'],
signalIndexName: 'siem-signals-default',
enableExperimental: parseExperimentalConfigValue([]),
});
const defaultState = {
defaultDataView: mockSourcererState.defaultDataView,
enableExperimental: parseExperimentalConfigValue([]),
kibanaDataViews: [mockSourcererState.defaultDataView],
signalIndexName: 'siem-signals-default',
};
const initState = createInitialState(mockPluginState, defaultState);
beforeEach(() => {
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(initState));
});
afterEach(() => {
(useDeepEqualSelector as jest.Mock).mockClear();
});
expect(initState.sourcerer?.sourcererScopes.default.indicesExist).toEqual(true);
test('indicesExist should be TRUE if configIndexPatterns is NOT empty', async () => {
const { result } = renderHook(() => useSourcererDataView());
expect(result.current.indicesExist).toEqual(true);
});
test('indicesExist should be FALSE if configIndexPatterns is empty', () => {
const initState = createInitialState(mockPluginState, {
kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }],
configIndexPatterns: [],
signalIndexName: 'siem-signals-default',
enableExperimental: parseExperimentalConfigValue([]),
const state = createInitialState(mockPluginState, {
...defaultState,
defaultDataView: {
...defaultState.defaultDataView,
id: '',
title: '',
patternList: [],
},
});
expect(initState.sourcerer?.sourcererScopes.default.indicesExist).toEqual(false);
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => cb(state));
const { result } = renderHook(() => useSourcererDataView());
expect(result.current.indicesExist).toEqual(false);
});
});
});

View file

@ -21,15 +21,15 @@ import { SecuritySubPlugins } from '../../app/types';
import { ManagementPluginReducer } from '../../management';
import { State } from './types';
import { AppAction } from './actions';
import { KibanaIndexPatterns } from './sourcerer/model';
import { initDataView, SourcererModel, SourcererScopeName } from './sourcerer/model';
import { ExperimentalFeatures } from '../../../common/experimental_features';
import { getScopePatternListSelection } from './sourcerer/helpers';
export type SubPluginsInitReducer = HostsPluginReducer &
UebaPluginReducer &
NetworkPluginReducer &
TimelinePluginReducer &
ManagementPluginReducer;
/**
* Factory for the 'initialState' that is used to preload state into the Security App's redux store.
*/
@ -39,17 +39,38 @@ export const createInitialState = (
'app' | 'dragAndDrop' | 'inputs' | 'sourcerer'
>,
{
kibanaIndexPatterns,
configIndexPatterns,
defaultDataView,
kibanaDataViews,
signalIndexName,
enableExperimental,
}: {
kibanaIndexPatterns: KibanaIndexPatterns;
configIndexPatterns: string[];
signalIndexName: string | null;
defaultDataView: SourcererModel['defaultDataView'];
kibanaDataViews: SourcererModel['kibanaDataViews'];
signalIndexName: SourcererModel['signalIndexName'];
enableExperimental: ExperimentalFeatures;
}
): State => {
const initialPatterns = {
[SourcererScopeName.default]: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.default,
signalIndexName,
true
),
[SourcererScopeName.detections]: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.detections,
signalIndexName,
true
),
[SourcererScopeName.timeline]: getScopePatternListSelection(
defaultDataView,
SourcererScopeName.timeline,
signalIndexName,
true
),
};
const preloadedState: State = {
...pluginsInitState,
app: { ...initialAppState, enableExperimental },
@ -59,13 +80,24 @@ export const createInitialState = (
...sourcererModel.initialSourcererState,
sourcererScopes: {
...sourcererModel.initialSourcererState.sourcererScopes,
default: {
[SourcererScopeName.default]: {
...sourcererModel.initialSourcererState.sourcererScopes.default,
indicesExist: configIndexPatterns.length > 0,
selectedDataViewId: defaultDataView.id,
selectedPatterns: initialPatterns[SourcererScopeName.default],
},
[SourcererScopeName.detections]: {
...sourcererModel.initialSourcererState.sourcererScopes.detections,
selectedDataViewId: defaultDataView.id,
selectedPatterns: initialPatterns[SourcererScopeName.detections],
},
[SourcererScopeName.timeline]: {
...sourcererModel.initialSourcererState.sourcererScopes.timeline,
selectedDataViewId: defaultDataView.id,
selectedPatterns: initialPatterns[SourcererScopeName.timeline],
},
},
kibanaIndexPatterns,
configIndexPatterns,
defaultDataView,
kibanaDataViews: kibanaDataViews.map((dataView) => ({ ...initDataView, ...dataView })),
signalIndexName,
},
};

View file

@ -8,35 +8,39 @@
import actionCreatorFactory from 'typescript-fsa';
import { TimelineEventsType } from '../../../../common/types/timeline';
import { KibanaIndexPatterns, ManageScopeInit, SourcererScopeName } from './model';
import { SourcererDataView, SourcererScopeName } from './model';
import { SecurityDataView } from '../../containers/sourcerer/api';
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer');
export const setSource = actionCreator<{
id: SourcererScopeName;
payload: ManageScopeInit;
}>('SET_SOURCE');
export const setDataView = actionCreator<{
browserFields: SourcererDataView['browserFields'];
docValueFields: SourcererDataView['docValueFields'];
id: SourcererDataView['id'];
indexFields: SourcererDataView['indexFields'];
loading: SourcererDataView['loading'];
runtimeMappings: SourcererDataView['runtimeMappings'];
}>('SET_DATA_VIEW');
export const setIndexPatternsList = actionCreator<{
kibanaIndexPatterns: KibanaIndexPatterns;
configIndexPatterns: string[];
}>('SET_INDEX_PATTERNS_LIST');
export const setDataViewLoading = actionCreator<{
id: string;
loading: boolean;
}>('SET_DATA_VIEW_LOADING');
export const setSignalIndexName =
actionCreator<{ signalIndexName: string }>('SET_SIGNAL_INDEX_NAME');
export const setSourcererScopeLoading = actionCreator<{ id: SourcererScopeName; loading: boolean }>(
'SET_SOURCERER_SCOPE_LOADING'
);
export const setSourcererDataViews = actionCreator<SecurityDataView>('SET_SOURCERER_DATA_VIEWS');
export const setSelectedIndexPatterns = actionCreator<{
export const setSourcererScopeLoading = actionCreator<{
id?: SourcererScopeName;
loading: boolean;
}>('SET_SOURCERER_SCOPE_LOADING');
export interface SelectedDataViewPayload {
id: SourcererScopeName;
selectedDataViewId: string;
selectedPatterns: string[];
eventType?: TimelineEventsType;
}>('SET_SELECTED_INDEX_PATTERNS');
export const initTimelineIndexPatterns = actionCreator<{
id: SourcererScopeName;
selectedPatterns: string[];
eventType?: TimelineEventsType;
}>('INIT_TIMELINE_INDEX_PATTERNS');
}
export const setSelectedDataView = actionCreator<SelectedDataViewPayload>('SET_SELECTED_DATA_VIEW');

View file

@ -5,59 +5,243 @@
* 2.0.
*/
import { createDefaultIndexPatterns, Args } from './helpers';
import { initialSourcererState, SourcererScopeName } from './model';
import { mockGlobalState } from '../../mock';
import { SourcererScopeName } from './model';
import {
defaultDataViewByEventType,
getScopePatternListSelection,
validateSelectedPatterns,
} from './helpers';
let defaultArgs: Args = {
eventType: 'all',
id: SourcererScopeName.default,
selectedPatterns: ['auditbeat-*', 'packetbeat-*'],
state: {
...initialSourcererState,
configIndexPatterns: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'],
kibanaIndexPatterns: [{ id: '123', title: 'journalbeat-*' }],
signalIndexName: 'signals-*',
},
const signalIndexName = mockGlobalState.sourcerer.signalIndexName;
const dataView = {
...mockGlobalState.sourcerer.defaultDataView,
title: `auditbeat-*,packetbeat-*,${signalIndexName}`,
patternList: ['packetbeat-*', 'auditbeat-*', `${signalIndexName}`],
};
const eventTypes: Array<Args['eventType']> = ['all', 'raw', 'alert', 'signal', 'custom'];
const ids: Array<Args['id']> = [
SourcererScopeName.default,
SourcererScopeName.detections,
SourcererScopeName.timeline,
];
describe('createDefaultIndexPatterns', () => {
ids.forEach((id) => {
eventTypes.forEach((et) => {
describe(`id: ${id}, eventType: ${et}`, () => {
beforeEach(() => {
defaultArgs = {
...defaultArgs,
id,
eventType: et,
};
const patternListNoSignals = mockGlobalState.sourcerer.defaultDataView.patternList
.filter((p) => p !== signalIndexName)
.sort();
const patternListSignals = [
signalIndexName,
...mockGlobalState.sourcerer.defaultDataView.patternList.filter((p) => p !== signalIndexName),
].sort();
describe('sourcerer store helpers', () => {
describe('getScopePatternListSelection', () => {
it('is not a default data view, returns patternList sorted', () => {
const result = getScopePatternListSelection(
{
...dataView,
id: '1234',
},
SourcererScopeName.default,
signalIndexName,
false
);
expect(result).toEqual([`${signalIndexName}`, 'auditbeat-*', 'packetbeat-*']);
});
it('default data view, SourcererScopeName.timeline, returns patternList sorted', () => {
const result = getScopePatternListSelection(
dataView,
SourcererScopeName.timeline,
signalIndexName,
true
);
expect(result).toEqual([signalIndexName, 'auditbeat-*', 'packetbeat-*']);
});
it('default data view, SourcererScopeName.default, returns patternList sorted without signals index', () => {
const result = getScopePatternListSelection(
dataView,
SourcererScopeName.default,
signalIndexName,
true
);
expect(result).toEqual(['auditbeat-*', 'packetbeat-*']);
});
it('default data view, SourcererScopeName.detections, returns patternList with only signals index', () => {
const result = getScopePatternListSelection(
dataView,
SourcererScopeName.detections,
signalIndexName,
true
);
expect(result).toEqual([signalIndexName]);
});
});
describe('validateSelectedPatterns', () => {
const payload = {
id: SourcererScopeName.default,
selectedDataViewId: dataView.id,
selectedPatterns: ['auditbeat-*'],
};
it('sets selectedPattern', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, payload);
expect(result).toEqual({
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedPatterns: ['auditbeat-*'],
},
});
});
it('sets to default when empty array is passed and scope is default', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
...payload,
selectedPatterns: [],
});
expect(result).toEqual({
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedPatterns: patternListNoSignals,
},
});
});
it('sets to default when empty array is passed and scope is detections', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
...payload,
id: SourcererScopeName.detections,
selectedPatterns: [],
});
expect(result).toEqual({
[SourcererScopeName.detections]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.detections],
selectedDataViewId: dataView.id,
selectedPatterns: [signalIndexName],
},
});
});
it('sets to default when empty array is passed and scope is timeline', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
...payload,
id: SourcererScopeName.timeline,
selectedPatterns: [],
});
expect(result).toEqual({
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: dataView.id,
selectedPatterns: [
signalIndexName,
...mockGlobalState.sourcerer.defaultDataView.patternList.filter(
(p) => p !== signalIndexName
),
].sort(),
},
});
});
describe('handles missing dataViewId, 7.16 -> 8.0', () => {
it('selectedPatterns.length > 0 & all selectedPatterns exist in defaultDataView, set dataViewId to defaultDataView.id', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
...payload,
id: SourcererScopeName.timeline,
selectedDataViewId: '',
selectedPatterns: [
mockGlobalState.sourcerer.defaultDataView.patternList[3],
mockGlobalState.sourcerer.defaultDataView.patternList[4],
],
});
it('Selected patterns', () => {
const result = createDefaultIndexPatterns(defaultArgs);
expect(result).toEqual(['auditbeat-*', 'packetbeat-*']);
expect(result).toEqual({
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: dataView.id,
selectedPatterns: [
mockGlobalState.sourcerer.defaultDataView.patternList[3],
mockGlobalState.sourcerer.defaultDataView.patternList[4],
],
},
});
it('No selected patterns', () => {
const newArgs = {
...defaultArgs,
selectedPatterns: [],
};
const result = createDefaultIndexPatterns(newArgs);
if (
id === SourcererScopeName.detections ||
(id === SourcererScopeName.timeline && (et === 'alert' || et === 'signal'))
) {
expect(result).toEqual(['signals-*']);
} else if (id === SourcererScopeName.timeline && et === 'all') {
expect(result).toEqual(['filebeat-*', 'auditbeat-*', 'packetbeat-*', 'signals-*']);
} else {
expect(result).toEqual(['filebeat-*', 'auditbeat-*', 'packetbeat-*']);
}
});
it('selectedPatterns.length > 0 & a pattern in selectedPatterns does not exist in defaultDataView, set dataViewId to null', () => {
const result = validateSelectedPatterns(mockGlobalState.sourcerer, {
...payload,
id: SourcererScopeName.timeline,
selectedDataViewId: '',
selectedPatterns: [
mockGlobalState.sourcerer.defaultDataView.patternList[3],
'journalbeat-*',
],
});
expect(result).toEqual({
[SourcererScopeName.timeline]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline],
selectedDataViewId: null,
selectedPatterns: [
mockGlobalState.sourcerer.defaultDataView.patternList[3],
'journalbeat-*',
],
},
});
});
});
});
describe('defaultDataViewByEventType', () => {
it('defaults with no eventType', () => {
const result = defaultDataViewByEventType({ state: mockGlobalState.sourcerer });
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
it('defaults with eventType: all', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'all',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
it('defaults with eventType: raw', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'raw',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListNoSignals,
});
});
it('defaults with eventType: alert', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'alert',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: [signalIndexName],
});
});
it('defaults with eventType: signal', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'signal',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: [signalIndexName],
});
});
it('defaults with eventType: custom', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'custom',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
it('defaults with eventType: eql', () => {
const result = defaultDataViewByEventType({
state: mockGlobalState.sourcerer,
eventType: 'eql',
});
expect(result).toEqual({
selectedDataViewId: dataView.id,
selectedPatterns: patternListSignals,
});
});
});
});

View file

@ -6,8 +6,9 @@
*/
import { isEmpty } from 'lodash';
import { SourcererModel, SourcererScopeName } from './model';
import { TimelineEventsType } from '../../../../common/types/timeline';
import { SourcererDataView, SourcererModel, SourcererScopeById, SourcererScopeName } from './model';
import { TimelineEventsType } from '../../../../common';
import { SelectedDataViewPayload } from './actions';
export interface Args {
eventType?: TimelineEventsType;
@ -15,40 +16,131 @@ export interface Args {
selectedPatterns: string[];
state: SourcererModel;
}
export const createDefaultIndexPatterns = ({ eventType, id, selectedPatterns, state }: Args) => {
const kibanaIndexPatterns = state.kibanaIndexPatterns.map((kip) => kip.title);
const newSelectedPatterns = selectedPatterns.filter(
(sp) =>
state.configIndexPatterns.includes(sp) ||
kibanaIndexPatterns.includes(sp) ||
(!isEmpty(state.signalIndexName) && state.signalIndexName === sp)
);
if (isEmpty(newSelectedPatterns)) {
let defaultIndexPatterns = state.configIndexPatterns;
if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) {
defaultIndexPatterns = defaultIndexPatternByEventType({ state, eventType });
} else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) {
defaultIndexPatterns = [state.signalIndexName ?? ''];
}
return defaultIndexPatterns;
export const getScopePatternListSelection = (
theDataView: SourcererDataView | undefined,
sourcererScope: SourcererScopeName,
signalIndexName: SourcererModel['signalIndexName'],
isDefaultDataView: boolean
): string[] => {
const patternList: string[] =
theDataView != null && theDataView.id !== null ? theDataView.patternList : [];
if (!isDefaultDataView) {
return patternList.sort();
}
// when our SIEM data view is set, here are the defaults
switch (sourcererScope) {
case SourcererScopeName.default:
return patternList.filter((index) => index !== signalIndexName).sort();
case SourcererScopeName.detections:
// set to signalIndexName whether or not it exists yet in the patternList
return (signalIndexName != null ? [signalIndexName] : []).sort();
case SourcererScopeName.timeline:
return (
signalIndexName != null
? [
// remove signalIndexName in case its already in there and add it whether or not it exists yet in the patternList
...patternList.filter((index) => index !== signalIndexName),
signalIndexName,
]
: patternList
).sort();
}
return newSelectedPatterns;
};
export const defaultIndexPatternByEventType = ({
export const validateSelectedPatterns = (
state: SourcererModel,
payload: SelectedDataViewPayload
): Partial<SourcererScopeById> => {
const { id, eventType, ...rest } = payload;
let dataView = state.kibanaDataViews.find((p) => p.id === rest.selectedDataViewId);
// dedupe because these could come from a silly url or pre 8.0 timeline
const dedupePatterns = [...new Set(rest.selectedPatterns)];
let selectedPatterns =
dataView != null
? dedupePatterns.filter(
(pattern) =>
// Typescript is being mean and telling me dataView could be undefined here
// so redoing the dataView != null check
(dataView != null && dataView.patternList.includes(pattern)) ||
// this is a hack, but sometimes signal index is deleted and is getting regenerated. it gets set before it is put in the dataView
state.signalIndexName == null
)
: // 7.16 -> 8.0 this will get hit because dataView == null
dedupePatterns;
if (selectedPatterns.length > 0 && dataView == null) {
// we have index patterns, but not a data view id
// find out if we have these index patterns in the defaultDataView
const areAllPatternsInDefault = selectedPatterns.every(
(pattern) => state.defaultDataView.title.indexOf(pattern) > -1
);
if (areAllPatternsInDefault) {
dataView = state.defaultDataView;
selectedPatterns = selectedPatterns.filter(
(pattern) => dataView != null && dataView.patternList.includes(pattern)
);
}
}
// TO DO: Steph/sourcerer If dataView is still undefined here, create temporary dataView
// and prompt user to go create this dataView
// currently UI will take the undefined dataView and default to defaultDataView anyways
// this is a "strategically merged" bug ;)
// https://github.com/elastic/security-team/issues/1921
return {
[id]: {
...state.sourcererScopes[id],
...rest,
selectedDataViewId: dataView?.id ?? null,
selectedPatterns,
...(isEmpty(selectedPatterns)
? id === SourcererScopeName.timeline
? defaultDataViewByEventType({ state, eventType })
: {
selectedPatterns: getScopePatternListSelection(
dataView ?? state.defaultDataView,
id,
state.signalIndexName,
(dataView ?? state.defaultDataView).id === state.defaultDataView.id
),
}
: {}),
loading: false,
},
};
};
// TODO: Steph/sourcerer eventType will be alerts only, when ui updates delete raw
export const defaultDataViewByEventType = ({
state,
eventType,
}: {
state: SourcererModel;
eventType?: TimelineEventsType;
}) => {
let defaultIndexPatterns = state.configIndexPatterns;
if (eventType === 'all' && !isEmpty(state.signalIndexName)) {
defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? ''];
const {
signalIndexName,
defaultDataView: { id, patternList },
} = state;
if (signalIndexName != null && (eventType === 'signal' || eventType === 'alert')) {
return {
selectedPatterns: [signalIndexName],
selectedDataViewId: id,
};
} else if (eventType === 'raw') {
defaultIndexPatterns = state.configIndexPatterns;
} else if (!isEmpty(state.signalIndexName) && (eventType === 'signal' || eventType === 'alert')) {
defaultIndexPatterns = [state.signalIndexName ?? ''];
return {
selectedPatterns: patternList.filter((index) => index !== signalIndexName).sort(),
selectedDataViewId: id,
};
}
return defaultIndexPatterns;
return {
selectedPatterns: [
// remove signalIndexName in case its already in there and add it whether or not it exists yet in the patternList
...patternList.filter((index) => index !== signalIndexName),
signalIndexName,
].sort(),
selectedDataViewId: id,
};
};

View file

@ -5,72 +5,133 @@
* 2.0.
*/
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
import { DocValueFields } from '../../../../common/search_strategy/common';
import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import {
BrowserFields,
DocValueFields,
EMPTY_BROWSER_FIELDS,
EMPTY_DOCVALUE_FIELD,
EMPTY_INDEX_PATTERN,
} from '../../../../common/search_strategy/index_fields';
export type ErrorModel = Error[];
EMPTY_INDEX_FIELDS,
} from '../../../../../timelines/common';
import { SecuritySolutionDataViewBase } from '../../types';
/** Uniquely identifies a Sourcerer Scope */
export enum SourcererScopeName {
default = 'default',
detections = 'detections',
timeline = 'timeline',
}
export interface ManageScope {
browserFields: BrowserFields;
docValueFields: DocValueFields[];
errorMessage: string | null;
/**
* Data related to each sourcerer scope
*/
export interface SourcererScope {
/** Uniquely identifies a Sourcerer Scope */
id: SourcererScopeName;
indexPattern: Omit<IIndexPattern, 'fieldFormatMap'>;
indicesExist: boolean | undefined | null;
/** is an update being made to the sourcerer data view */
loading: boolean;
/** selected data view id */
selectedDataViewId: string;
/** selected patterns within the data view */
selectedPatterns: string[];
}
export interface ManageScopeInit extends Partial<ManageScope> {
id: SourcererScopeName;
export type SourcererScopeById = Record<SourcererScopeName, SourcererScope>;
export interface KibanaDataView {
/** Uniquely identifies a Kibana Data View */
id: string;
/** list of active patterns that return data */
patternList: string[];
/**
* title of Kibana Data View
* title also serves as "all pattern list", including inactive
* comma separated string
*/
title: string;
}
export type SourcererScopeById = Record<SourcererScopeName | string, ManageScope>;
/**
* DataView from Kibana + timelines/index_fields enhanced field data
*/
export interface SourcererDataView extends KibanaDataView {
/** we need this for @timestamp data */
browserFields: BrowserFields;
/** we need this for @timestamp data */
docValueFields: DocValueFields[];
/** comes from dataView.fields.toSpec() */
indexFields: SecuritySolutionDataViewBase['fields'];
/** set when data view fields are fetched */
loading: boolean;
/**
* Needed to pass to search strategy
* Remove once issue resolved: https://github.com/elastic/kibana/issues/111762
*/
runtimeMappings: MappingRuntimeFields;
}
export type KibanaIndexPatterns = Array<{ id: string; title: string }>;
/**
* Combined data from SourcererDataView and SourcererScope to create
* selected data view state
*/
export interface SelectedDataView {
browserFields: SourcererDataView['browserFields'];
dataViewId: SourcererDataView['id'];
docValueFields: SourcererDataView['docValueFields'];
/**
* DataViewBase with enhanced index fields used in timelines
*/
indexPattern: SecuritySolutionDataViewBase;
/** do the selected indices exist */
indicesExist: boolean;
/** is an update being made to the data view */
loading: boolean;
/** all active & inactive patterns from SourcererDataView['title'] */
patternList: string[];
runtimeMappings: SourcererDataView['runtimeMappings'];
/** all selected patterns from SourcererScope['selectedPatterns'] */
selectedPatterns: string[];
}
// ManageSourcerer
/**
* sourcerer model for redux
*/
export interface SourcererModel {
kibanaIndexPatterns: KibanaIndexPatterns;
configIndexPatterns: string[];
/** default security-solution data view */
defaultDataView: SourcererDataView & { error?: unknown };
/** all Kibana data views, including security-solution */
kibanaDataViews: SourcererDataView[];
/** security solution signals index name */
signalIndexName: string | null;
/** sourcerer scope data by id */
sourcererScopes: SourcererScopeById;
}
export const initSourcererScope: Pick<
ManageScope,
| 'browserFields'
| 'docValueFields'
| 'errorMessage'
| 'indexPattern'
| 'indicesExist'
| 'loading'
| 'selectedPatterns'
> = {
export type SourcererUrlState = Partial<{
[id in SourcererScopeName]: {
id: string;
selectedPatterns: string[];
};
}>;
export const initSourcererScope: Omit<SourcererScope, 'id'> = {
loading: false,
selectedDataViewId: '',
selectedPatterns: [],
};
export const initDataView = {
browserFields: EMPTY_BROWSER_FIELDS,
docValueFields: EMPTY_DOCVALUE_FIELD,
errorMessage: null,
indexPattern: EMPTY_INDEX_PATTERN,
indicesExist: true,
id: '',
indexFields: EMPTY_INDEX_FIELDS,
loading: false,
selectedPatterns: [],
patternList: [],
runtimeMappings: {},
title: '',
};
export const initialSourcererState: SourcererModel = {
kibanaIndexPatterns: [],
configIndexPatterns: [],
defaultDataView: initDataView,
kibanaDataViews: [],
signalIndexName: null,
sourcererScopes: {
[SourcererScopeName.default]: {
@ -87,8 +148,3 @@ export const initialSourcererState: SourcererModel = {
},
},
};
export type FSourcererScopePatterns = {
[id in SourcererScopeName]: string[];
};
export type SourcererScopePatterns = Partial<FSourcererScopePatterns>;

View file

@ -5,83 +5,89 @@
* 2.0.
*/
import { isEmpty } from 'lodash/fp';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import {
setIndexPatternsList,
setSourcererDataViews,
setSourcererScopeLoading,
setSelectedIndexPatterns,
setSelectedDataView,
setSignalIndexName,
setSource,
initTimelineIndexPatterns,
setDataView,
setDataViewLoading,
} from './actions';
import { initialSourcererState, SourcererModel } from './model';
import { createDefaultIndexPatterns, defaultIndexPatternByEventType } from './helpers';
import { initDataView, initialSourcererState, SourcererModel, SourcererScopeName } from './model';
import { validateSelectedPatterns } from './helpers';
export type SourcererState = SourcererModel;
export const sourcererReducer = reducerWithInitialState(initialSourcererState)
.case(setIndexPatternsList, (state, { kibanaIndexPatterns, configIndexPatterns }) => ({
...state,
kibanaIndexPatterns,
configIndexPatterns,
}))
.case(setSignalIndexName, (state, { signalIndexName }) => ({
...state,
signalIndexName,
}))
.case(setDataViewLoading, (state, { id, loading }) => ({
...state,
...(id === state.defaultDataView.id
? {
defaultDataView: { ...state.defaultDataView, loading },
}
: {}),
kibanaDataViews: state.kibanaDataViews.map((dv) => (dv.id === id ? { ...dv, loading } : dv)),
}))
.case(setSourcererDataViews, (state, { defaultDataView, kibanaDataViews }) => ({
...state,
defaultDataView: {
...state.defaultDataView,
...defaultDataView,
},
kibanaDataViews: kibanaDataViews.map((dataView) => ({
...(state.kibanaDataViews.find(({ id }) => id === dataView.id) ?? initDataView),
...dataView,
})),
}))
.case(setSourcererScopeLoading, (state, { id, loading }) => ({
...state,
sourcererScopes: {
...state.sourcererScopes,
[id]: {
...state.sourcererScopes[id],
loading,
},
...(id != null
? {
[id]: {
...state.sourcererScopes[id],
loading,
},
}
: {
[SourcererScopeName.default]: {
...state.sourcererScopes[SourcererScopeName.default],
loading,
},
[SourcererScopeName.detections]: {
...state.sourcererScopes[SourcererScopeName.detections],
loading,
},
[SourcererScopeName.timeline]: {
...state.sourcererScopes[SourcererScopeName.timeline],
loading,
},
}),
},
}))
.case(setSelectedIndexPatterns, (state, { id, selectedPatterns, eventType }) => {
return {
...state,
sourcererScopes: {
...state.sourcererScopes,
[id]: {
...state.sourcererScopes[id],
selectedPatterns: createDefaultIndexPatterns({ eventType, id, selectedPatterns, state }),
},
},
};
})
.case(initTimelineIndexPatterns, (state, { id, selectedPatterns, eventType }) => {
return {
...state,
sourcererScopes: {
...state.sourcererScopes,
[id]: {
...state.sourcererScopes[id],
selectedPatterns: isEmpty(selectedPatterns)
? defaultIndexPatternByEventType({ state, eventType })
: selectedPatterns,
},
},
};
})
.case(setSource, (state, { id, payload }) => {
const { ...sourcererScopes } = payload;
return {
...state,
sourcererScopes: {
...state.sourcererScopes,
[id]: {
...state.sourcererScopes[id],
...sourcererScopes,
...(state.sourcererScopes[id].selectedPatterns.length === 0
? { selectedPatterns: state.configIndexPatterns }
: {}),
},
},
};
})
.case(setSelectedDataView, (state, payload) => ({
...state,
sourcererScopes: {
...state.sourcererScopes,
...validateSelectedPatterns(state, payload),
},
}))
.case(setDataView, (state, dataView) => ({
...state,
...(dataView.id === state.defaultDataView.id
? {
defaultDataView: { ...state.defaultDataView, ...dataView },
}
: {}),
kibanaDataViews: state.kibanaDataViews.map((dv) =>
dv.id === dataView.id ? { ...dv, ...dataView } : dv
),
}))
.build();

View file

@ -1,75 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { cloneDeep } from 'lodash/fp';
import { mockGlobalState } from '../../mock';
import { SourcererScopeName } from './model';
import { getSourcererScopeSelector } from './selectors';
describe('Sourcerer selectors', () => {
describe('getSourcererScopeSelector', () => {
it('Should exclude elastic cloud alias when selected patterns include "logs-*" as an alias', () => {
const mapStateToProps = getSourcererScopeSelector();
expect(mapStateToProps(mockGlobalState, SourcererScopeName.default).selectedPatterns).toEqual(
[
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
'-*elastic-cloud-logs-*',
]
);
});
it('Should NOT exclude elastic cloud alias when selected patterns does NOT include "logs-*" as an alias', () => {
const mapStateToProps = getSourcererScopeSelector();
const myMockGlobalState = cloneDeep(mockGlobalState);
myMockGlobalState.sourcerer.sourcererScopes.default.selectedPatterns =
myMockGlobalState.sourcerer.sourcererScopes.default.selectedPatterns.filter(
(index) => !index.includes('logs-*')
);
expect(
mapStateToProps(myMockGlobalState, SourcererScopeName.default).selectedPatterns
).toEqual([
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
]);
});
it('Should NOT exclude elastic cloud alias when selected patterns include "logs-endpoint.event-*" as an alias', () => {
const mapStateToProps = getSourcererScopeSelector();
const myMockGlobalState = cloneDeep(mockGlobalState);
myMockGlobalState.sourcerer.sourcererScopes.default.selectedPatterns = [
...myMockGlobalState.sourcerer.sourcererScopes.default.selectedPatterns.filter(
(index) => !index.includes('logs-*')
),
'logs-endpoint.event-*',
];
expect(
mapStateToProps(myMockGlobalState, SourcererScopeName.default).selectedPatterns
).toEqual([
'apm-*-transaction*',
'auditbeat-*',
'endgame-*',
'filebeat-*',
'logs-endpoint.event-*',
'packetbeat-*',
'traces-apm*',
'winlogbeat-*',
]);
});
});
});

View file

@ -5,24 +5,34 @@
* 2.0.
*/
import memoizeOne from 'memoize-one';
import { createSelector } from 'reselect';
import { State } from '../types';
import { SourcererScopeById, ManageScope, KibanaIndexPatterns, SourcererScopeName } from './model';
import {
SourcererDataView,
SourcererModel,
SourcererScope,
SourcererScopeById,
SourcererScopeName,
} from './model';
export const sourcererKibanaIndexPatternsSelector = ({ sourcerer }: State): KibanaIndexPatterns =>
sourcerer.kibanaIndexPatterns;
export const sourcererKibanaDataViewsSelector = ({
sourcerer,
}: State): SourcererModel['kibanaDataViews'] => sourcerer.kibanaDataViews;
export const sourcererSignalIndexNameSelector = ({ sourcerer }: State): string | null =>
sourcerer.signalIndexName;
export const sourcererConfigIndexPatternsSelector = ({ sourcerer }: State): string[] =>
sourcerer.configIndexPatterns;
export const sourcererDefaultDataViewSelector = ({
sourcerer,
}: State): SourcererModel['defaultDataView'] => sourcerer.defaultDataView;
export const dataViewSelector = ({ sourcerer }: State, id: string): SourcererDataView =>
sourcerer.kibanaDataViews.find((dataView) => dataView.id === id) ?? sourcerer.defaultDataView;
export const sourcererScopeIdSelector = (
{ sourcerer }: State,
scopeId: SourcererScopeName
): ManageScope => sourcerer.sourcererScopes[scopeId];
): SourcererScope => sourcerer.sourcererScopes[scopeId];
export const scopeIdSelector = () => createSelector(sourcererScopeIdSelector, (scope) => scope);
@ -31,85 +41,43 @@ export const sourcererScopesSelector = ({ sourcerer }: State): SourcererScopeByI
export const scopesSelector = () => createSelector(sourcererScopesSelector, (scopes) => scopes);
export const kibanaIndexPatternsSelector = () =>
createSelector(
sourcererKibanaIndexPatternsSelector,
(kibanaIndexPatterns) => kibanaIndexPatterns
);
export const kibanaDataViewsSelector = () =>
createSelector(sourcererKibanaDataViewsSelector, (dataViews) => dataViews);
export const signalIndexNameSelector = () =>
createSelector(sourcererSignalIndexNameSelector, (signalIndexName) => signalIndexName);
export const configIndexPatternsSelector = () =>
createSelector(
sourcererConfigIndexPatternsSelector,
(configIndexPatterns) => configIndexPatterns
);
export const defaultDataViewSelector = () =>
createSelector(sourcererDefaultDataViewSelector, (dataViews) => dataViews);
export const getIndexNamesSelectedSelector = () => {
const getScopeSelector = scopeIdSelector();
const getConfigIndexPatternsSelector = configIndexPatternsSelector();
export const sourcererDataViewSelector = () =>
createSelector(dataViewSelector, (dataView) => dataView);
const mapStateToProps = (
state: State,
scopeId: SourcererScopeName
): { indexNames: string[]; previousIndexNames: string } => {
const scope = getScopeSelector(state, scopeId);
const configIndexPatterns = getConfigIndexPatternsSelector(state);
return {
indexNames:
scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns,
previousIndexNames: scope.indexPattern.title,
};
};
return mapStateToProps;
};
export const getAllExistingIndexNamesSelector = () => {
const getSignalIndexNameSelector = signalIndexNameSelector();
const getConfigIndexPatternsSelector = configIndexPatternsSelector();
const mapStateToProps = (state: State): string[] => {
const signalIndexName = getSignalIndexNameSelector(state);
const configIndexPatterns = getConfigIndexPatternsSelector(state);
return signalIndexName != null
? [...configIndexPatterns, signalIndexName]
: configIndexPatterns;
};
return mapStateToProps;
};
const EXCLUDE_ELASTIC_CLOUD_INDEX = '-*elastic-cloud-logs-*';
export interface SourcererScopeSelector extends Omit<SourcererModel, 'sourcererScopes'> {
sourcererDataView: SourcererDataView;
sourcererScope: SourcererScope;
}
export const getSourcererScopeSelector = () => {
const getScopeIdSelector = scopeIdSelector();
const getSelectedPatterns = memoizeOne((selectedPatternsStr: string): string[] => {
const selectedPatterns = selectedPatternsStr.length > 0 ? selectedPatternsStr.split(',') : [];
return selectedPatterns.some((index) => index === 'logs-*')
? [...selectedPatterns, EXCLUDE_ELASTIC_CLOUD_INDEX]
: selectedPatterns;
});
const getKibanaDataViewsSelector = kibanaDataViewsSelector();
const getDefaultDataViewSelector = defaultDataViewSelector();
const getSignalIndexNameSelector = signalIndexNameSelector();
const getSourcererDataViewSelector = sourcererDataViewSelector();
const getScopeSelector = scopeIdSelector();
const getIndexPattern = memoizeOne(
(indexPattern, title) => ({
...indexPattern,
title,
}),
(newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length
);
const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope => {
const scope = getScopeIdSelector(state, scopeId);
const selectedPatterns = getSelectedPatterns(scope.selectedPatterns.sort().join());
const indexPattern = getIndexPattern(scope.indexPattern, selectedPatterns.join());
return (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => {
const kibanaDataViews = getKibanaDataViewsSelector(state);
const defaultDataView = getDefaultDataViewSelector(state);
const signalIndexName = getSignalIndexNameSelector(state);
const scope = getScopeSelector(state, scopeId);
const sourcererDataView = getSourcererDataViewSelector(state, scope.selectedDataViewId);
return {
...scope,
selectedPatterns,
indexPattern,
defaultDataView,
kibanaDataViews,
signalIndexName,
sourcererDataView,
sourcererScope: scope,
};
};
return mapStateToProps;
};

View file

@ -6,6 +6,9 @@
*/
import { ResponseErrorAttributes } from 'kibana/server';
import { DataViewBase } from '@kbn/es-query';
import { FieldSpec } from '../../../../../src/plugins/data_views/common';
export interface ServerApiError {
statusCode: number;
error: string;
@ -16,3 +19,10 @@ export interface ServerApiError {
export interface SecuritySolutionUiConfigType {
enableExperimental: string[];
}
/**
* DataViewBase with enhanced index fields used in timelines
*/
export interface SecuritySolutionDataViewBase extends DataViewBase {
fields: FieldSpec[];
}

View file

@ -16,7 +16,7 @@ jest.mock('../route/use_route_spy', () => ({
.mockImplementationOnce(() => [{ pageName: 'network' }]),
}));
jest.mock('../../../common/containers/sourcerer', () => ({
useSourcererScope: jest
useSourcererDataView: jest
.fn()
.mockImplementationOnce(() => [{ indicesExist: false }])
.mockImplementationOnce(() => [{ indicesExist: false }])

View file

@ -8,7 +8,7 @@
import { useState, useEffect } from 'react';
import { useRouteSpy } from '../route/use_route_spy';
import { SecurityPageName } from '../../../app/types';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { useSourcererDataView } from '../../containers/sourcerer';
// Used to detect if we're on a top level page that is empty and set page background color to match the subdued Empty State
const isPageNameWithEmptyView = (currentName: string) => {
@ -23,7 +23,7 @@ const isPageNameWithEmptyView = (currentName: string) => {
export const useShowPagesWithEmptyView = () => {
const [{ pageName }] = useRouteSpy();
const { indicesExist } = useSourcererScope();
const { indicesExist } = useSourcererDataView();
const shouldShowEmptyState = isPageNameWithEmptyView(pageName) && !indicesExist;

Some files were not shown because too many files have changed in this diff Show more