diff --git a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_2x.png b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_2x.png new file mode 100644 index 000000000000..e8a8d08c78fe Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_dark_2x.png b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_dark_2x.png deleted file mode 100644 index 86ac827f06a7..000000000000 Binary files a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_dark_2x.png and /dev/null differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_light_2x.png b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_light_2x.png deleted file mode 100644 index 527a09aad05e..000000000000 Binary files a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_light_2x.png and /dev/null differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_observability_2x.png b/src/plugins/kibana_overview/public/assets/solutions_observability_2x.png new file mode 100644 index 000000000000..d73b3e311f9f Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_observability_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_observability_dark_2x.png b/src/plugins/kibana_overview/public/assets/solutions_observability_dark_2x.png deleted file mode 100644 index c9dd85ee07f3..000000000000 Binary files a/src/plugins/kibana_overview/public/assets/solutions_observability_dark_2x.png and /dev/null differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_observability_light_2x.png b/src/plugins/kibana_overview/public/assets/solutions_observability_light_2x.png deleted file mode 100644 index 85120b906c96..000000000000 Binary files a/src/plugins/kibana_overview/public/assets/solutions_observability_light_2x.png and /dev/null differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_security_solution_2x.png b/src/plugins/kibana_overview/public/assets/solutions_security_solution_2x.png new file mode 100644 index 000000000000..771bbb9790c4 Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_security_solution_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_security_solution_dark_2x.png b/src/plugins/kibana_overview/public/assets/solutions_security_solution_dark_2x.png deleted file mode 100644 index 24f902bff090..000000000000 Binary files a/src/plugins/kibana_overview/public/assets/solutions_security_solution_dark_2x.png and /dev/null differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_security_solution_light_2x.png b/src/plugins/kibana_overview/public/assets/solutions_security_solution_light_2x.png deleted file mode 100644 index 2b35af848f91..000000000000 Binary files a/src/plugins/kibana_overview/public/assets/solutions_security_solution_light_2x.png and /dev/null differ diff --git a/src/plugins/kibana_overview/public/components/_overview.scss b/src/plugins/kibana_overview/public/components/_overview.scss index 94555013d0a7..12e2d9cd921e 100644 --- a/src/plugins/kibana_overview/public/components/_overview.scss +++ b/src/plugins/kibana_overview/public/components/_overview.scss @@ -59,6 +59,24 @@ } } +.kbnOverviewSolution { + &.enterpriseSearch { + .euiCard__image { + background-color: $euiColorSecondary; + } + } + &.observability { + .euiCard__image { + background-color: $euiColorAccent; + } + } + &.securitySolution { + .euiCard__image { + background-color: $euiColorDarkestShade; + } + } +} + .kbnOverviewSolution__icon { background-color: $euiColorEmptyShade !important; box-shadow: none !important; diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 142fe37ae932..2e7dc9a7ddc6 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -13,25 +13,25 @@ exports[`Overview render 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -41,7 +41,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -49,7 +49,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -57,7 +57,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -65,7 +65,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -191,7 +191,7 @@ exports[`Overview render 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_kibana_2x.png" onClick={[Function]} title="Kibana" titleElement="h3" @@ -217,7 +217,7 @@ exports[`Overview render 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_solution_2_2x.png" onClick={[Function]} title="Solution two" titleElement="h3" @@ -243,7 +243,7 @@ exports[`Overview render 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_solution_3_2x.png" onClick={[Function]} title="Solution three" titleElement="h3" @@ -269,7 +269,7 @@ exports[`Overview render 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_solution_4_2x.png" onClick={[Function]} title="Solution four" titleElement="h3" @@ -305,25 +305,25 @@ exports[`Overview render 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -333,7 +333,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -341,7 +341,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -349,7 +349,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -357,7 +357,7 @@ exports[`Overview render 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -383,49 +383,49 @@ exports[`Overview without features 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], Array [ "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -435,7 +435,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -443,7 +443,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -451,7 +451,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -459,7 +459,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, Object { "type": "return", @@ -467,7 +467,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -475,7 +475,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -483,7 +483,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -491,7 +491,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -617,7 +617,7 @@ exports[`Overview without features 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_kibana_2x.png" onClick={[Function]} title="Kibana" titleElement="h3" @@ -643,7 +643,7 @@ exports[`Overview without features 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_solution_2_2x.png" onClick={[Function]} title="Solution two" titleElement="h3" @@ -669,7 +669,7 @@ exports[`Overview without features 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_solution_3_2x.png" onClick={[Function]} title="Solution three" titleElement="h3" @@ -695,7 +695,7 @@ exports[`Overview without features 1`] = ` > } - image="/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png" + image="/plugins/kibanaOverview/assets/solutions_solution_4_2x.png" onClick={[Function]} title="Solution four" titleElement="h3" @@ -731,49 +731,49 @@ exports[`Overview without features 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], Array [ "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -783,7 +783,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -791,7 +791,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -799,7 +799,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -807,7 +807,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, Object { "type": "return", @@ -815,7 +815,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -823,7 +823,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -831,7 +831,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -839,7 +839,7 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -865,25 +865,25 @@ exports[`Overview without solutions 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -893,7 +893,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -901,7 +901,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -909,7 +909,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -917,7 +917,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -1026,25 +1026,25 @@ exports[`Overview without solutions 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -1054,7 +1054,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -1062,7 +1062,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -1070,7 +1070,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -1078,7 +1078,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -1095,25 +1095,25 @@ exports[`Overview without solutions 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -1123,7 +1123,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -1131,7 +1131,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -1139,7 +1139,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -1147,7 +1147,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } @@ -1170,25 +1170,25 @@ exports[`Overview without solutions 1`] = ` "kibana_landing_page", ], Array [ - "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", ], Array [ "path-to-solution-two", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", ], Array [ "path-to-solution-three", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", ], Array [ "path-to-solution-four", ], Array [ - "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", ], ], "results": Array [ @@ -1198,7 +1198,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_kibana_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_kibana_2x.png", }, Object { "type": "return", @@ -1206,7 +1206,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_2_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_2_2x.png", }, Object { "type": "return", @@ -1214,7 +1214,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_3_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_3_2x.png", }, Object { "type": "return", @@ -1222,7 +1222,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/plugins/kibanaOverview/assets/solutions_solution_4_light_2x.png", + "value": "/plugins/kibanaOverview/assets/solutions_solution_4_2x.png", }, ], } diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 43f7dc82a6bd..68c52b039559 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -64,9 +64,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => .sort(sortByOrder); const getSolutionGraphicURL = (solutionId: string) => - `/plugins/${PLUGIN_ID}/assets/solutions_${solutionId}_${ - IS_DARK_THEME ? 'dark' : 'light' - }_2x.png`; + `/plugins/${PLUGIN_ID}/assets/solutions_${solutionId}_2x.png`; const findFeatureById = (featureId: string) => features.find(({ id }) => id === featureId); const kibanaApps = features.filter(({ solutionId }) => solutionId === 'kibana').sort(sortByOrder); @@ -199,7 +197,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => { proxyOnlyHosts: undefined, maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration('60s'), + cleanupFailedExecutionsTask: { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }, }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 1b9de0162f34..70c8b0e8185d 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { schema } from '@kbn/config-schema'; import { ByteSizeValue } from '@kbn/config-schema'; import { ActionsConfig } from './config'; import { @@ -24,6 +25,12 @@ const defaultActionsConfig: ActionsConfig = { rejectUnauthorized: true, maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration(60000), + cleanupFailedExecutionsTask: { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }, }; describe('ensureUriAllowed', () => { diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/cleanup_tasks.test.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/cleanup_tasks.test.ts new file mode 100644 index 000000000000..07c09a2dfef7 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/cleanup_tasks.test.ts @@ -0,0 +1,126 @@ +/* + * 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 type { SavedObjectsFindResult, SavedObjectsSerializer } from 'kibana/server'; +import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; +import { spacesMock } from '../../../spaces/server/mocks'; +import { CleanupTasksOpts, cleanupTasks } from './cleanup_tasks'; +import { TaskInstance } from '../../../task_manager/server'; +import { ApiResponse, estypes } from '@elastic/elasticsearch'; + +describe('cleanupTasks', () => { + const logger = loggingSystemMock.create().get(); + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + const spaces = spacesMock.createStart(); + const savedObjectsSerializer = ({ + generateRawId: jest + .fn() + .mockImplementation((namespace: string | undefined, type: string, id: string) => { + const namespacePrefix = namespace ? `${namespace}:` : ''; + return `${namespacePrefix}${type}:${id}`; + }), + } as unknown) as SavedObjectsSerializer; + + const cleanupTasksOpts: CleanupTasksOpts = { + logger, + esClient, + spaces, + savedObjectsSerializer, + kibanaIndex: '.kibana', + taskManagerIndex: '.kibana_task_manager', + tasks: [], + }; + + const taskSO: SavedObjectsFindResult = { + id: '123', + type: 'task', + references: [], + score: 0, + attributes: { + id: '123', + taskType: 'foo', + scheduledAt: new Date(), + state: {}, + runAt: new Date(), + startedAt: new Date(), + retryAt: new Date(), + ownerId: '234', + params: { spaceId: undefined, actionTaskParamsId: '123' }, + schedule: { interval: '5m' }, + }, + }; + + beforeEach(() => { + esClient.bulk.mockReset(); + }); + + it('should skip cleanup when there are no tasks to cleanup', async () => { + const result = await cleanupTasks(cleanupTasksOpts); + expect(result).toEqual({ + success: true, + successCount: 0, + failureCount: 0, + }); + expect(esClient.bulk).not.toHaveBeenCalled(); + }); + + it('should delete action_task_params and task objects', async () => { + esClient.bulk.mockResolvedValue(({ + body: { items: [], errors: false, took: 1 }, + } as unknown) as ApiResponse); + const result = await cleanupTasks({ + ...cleanupTasksOpts, + tasks: [taskSO], + }); + expect(esClient.bulk).toHaveBeenCalledWith({ + body: [{ delete: { _index: cleanupTasksOpts.kibanaIndex, _id: 'action_task_params:123' } }], + }); + expect(esClient.bulk).toHaveBeenCalledWith({ + body: [{ delete: { _index: cleanupTasksOpts.taskManagerIndex, _id: 'task:123' } }], + }); + expect(result).toEqual({ + success: true, + successCount: 1, + failureCount: 0, + }); + }); + + it('should not delete the task if the action_task_params failed to delete', async () => { + esClient.bulk.mockResolvedValue(({ + body: { + items: [ + { + delete: { + _index: cleanupTasksOpts.kibanaIndex, + _id: 'action_task_params:123', + status: 500, + result: 'Failure', + error: true, + }, + }, + ], + errors: true, + took: 1, + }, + } as unknown) as ApiResponse); + const result = await cleanupTasks({ + ...cleanupTasksOpts, + tasks: [taskSO], + }); + expect(esClient.bulk).toHaveBeenCalledWith({ + body: [{ delete: { _index: cleanupTasksOpts.kibanaIndex, _id: 'action_task_params:123' } }], + }); + expect(esClient.bulk).not.toHaveBeenCalledWith({ + body: [{ delete: { _index: cleanupTasksOpts.taskManagerIndex, _id: 'task:123' } }], + }); + expect(result).toEqual({ + success: false, + successCount: 0, + failureCount: 1, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/cleanup_tasks.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/cleanup_tasks.ts new file mode 100644 index 000000000000..3009bfe1a277 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/cleanup_tasks.ts @@ -0,0 +1,109 @@ +/* + * 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 { + Logger, + ElasticsearchClient, + SavedObjectsFindResult, + SavedObjectsSerializer, +} from 'kibana/server'; +import { TaskInstance } from '../../../task_manager/server'; +import { SpacesPluginStart } from '../../../spaces/server'; +import { + bulkDelete, + extractBulkResponseDeleteFailures, + getRawActionTaskParamsIdFromTask, +} from './lib'; + +export interface CleanupTasksOpts { + logger: Logger; + esClient: ElasticsearchClient; + tasks: Array>; + spaces?: SpacesPluginStart; + savedObjectsSerializer: SavedObjectsSerializer; + kibanaIndex: string; + taskManagerIndex: string; +} + +export interface CleanupTasksResult { + success: boolean; + successCount: number; + failureCount: number; +} + +/** + * Cleanup tasks + * + * This function receives action execution tasks that are in a failed state, removes + * the linked "action_task_params" object first and then if successful, the task manager's task. + */ +export async function cleanupTasks({ + logger, + esClient, + tasks, + spaces, + savedObjectsSerializer, + kibanaIndex, + taskManagerIndex, +}: CleanupTasksOpts): Promise { + const deserializedTasks = tasks.map((task) => ({ + ...task, + attributes: { + ...task.attributes, + params: + typeof task.attributes.params === 'string' + ? JSON.parse(task.attributes.params) + : task.attributes.params || {}, + }, + })); + + // Remove accumulated action task params + const actionTaskParamIdsToDelete = deserializedTasks.map((task) => + getRawActionTaskParamsIdFromTask({ task, spaces, savedObjectsSerializer }) + ); + const actionTaskParamBulkDeleteResult = await bulkDelete( + esClient, + kibanaIndex, + actionTaskParamIdsToDelete + ); + const failedActionTaskParams = actionTaskParamBulkDeleteResult + ? extractBulkResponseDeleteFailures(actionTaskParamBulkDeleteResult) + : []; + if (failedActionTaskParams?.length) { + logger.debug( + `Failed to delete the following action_task_params [${JSON.stringify( + failedActionTaskParams + )}]` + ); + } + + // Remove accumulated tasks + const taskIdsToDelete = deserializedTasks + .map((task) => { + const rawId = getRawActionTaskParamsIdFromTask({ task, spaces, savedObjectsSerializer }); + // Avoid removing tasks that failed to remove linked objects + if (failedActionTaskParams?.find((item) => item._id === rawId)) { + return null; + } + const rawTaskId = savedObjectsSerializer.generateRawId(undefined, 'task', task.id); + return rawTaskId; + }) + .filter((id) => !!id) as string[]; + const taskBulkDeleteResult = await bulkDelete(esClient, taskManagerIndex, taskIdsToDelete); + const failedTasks = taskBulkDeleteResult + ? extractBulkResponseDeleteFailures(taskBulkDeleteResult) + : []; + if (failedTasks?.length) { + logger.debug(`Failed to delete the following tasks [${JSON.stringify(failedTasks)}]`); + } + + return { + success: failedActionTaskParams?.length === 0 && failedTasks.length === 0, + successCount: tasks.length - failedActionTaskParams.length - failedTasks.length, + failureCount: failedActionTaskParams.length + failedTasks.length, + }; +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/constants.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/constants.ts new file mode 100644 index 000000000000..c8c1d6105586 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/constants.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export const TASK_TYPE = 'cleanup_failed_action_executions'; +export const TASK_ID = `Actions-${TASK_TYPE}`; diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/ensure_scheduled.test.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/ensure_scheduled.test.ts new file mode 100644 index 000000000000..3c27a38e818e --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/ensure_scheduled.test.ts @@ -0,0 +1,55 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { ActionsConfig } from '../config'; +import { ensureScheduled } from './ensure_scheduled'; +import { taskManagerMock } from '../../../task_manager/server/mocks'; +import { loggingSystemMock } from '../../../../../src/core/server/mocks'; + +describe('ensureScheduled', () => { + const logger = loggingSystemMock.create().get(); + const taskManager = taskManagerMock.createStart(); + + const config: ActionsConfig['cleanupFailedExecutionsTask'] = { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }; + + beforeEach(() => jest.resetAllMocks()); + + it(`should call task manager's ensureScheduled function with proper params`, async () => { + await ensureScheduled(taskManager, logger, config); + expect(taskManager.ensureScheduled).toHaveBeenCalledTimes(1); + expect(taskManager.ensureScheduled.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "id": "Actions-cleanup_failed_action_executions", + "params": Object {}, + "schedule": Object { + "interval": "5m", + }, + "state": Object { + "runs": 0, + "total_cleaned_up": 0, + }, + "taskType": "cleanup_failed_action_executions", + }, + ] + `); + }); + + it('should log an error and not throw when ensureScheduled function throws', async () => { + taskManager.ensureScheduled.mockRejectedValue(new Error('Fail')); + await ensureScheduled(taskManager, logger, config); + expect(logger.error).toHaveBeenCalledWith( + 'Error scheduling Actions-cleanup_failed_action_executions, received Fail' + ); + }); +}); diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/ensure_scheduled.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/ensure_scheduled.ts new file mode 100644 index 000000000000..6dc1ce44982c --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/ensure_scheduled.ts @@ -0,0 +1,34 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { TASK_ID, TASK_TYPE } from './constants'; +import { ActionsConfig } from '../config'; +import { TaskManagerStartContract, asInterval } from '../../../task_manager/server'; + +export async function ensureScheduled( + taskManager: TaskManagerStartContract, + logger: Logger, + { cleanupInterval }: ActionsConfig['cleanupFailedExecutionsTask'] +) { + try { + await taskManager.ensureScheduled({ + id: TASK_ID, + taskType: TASK_TYPE, + schedule: { + interval: asInterval(cleanupInterval.asMilliseconds()), + }, + state: { + runs: 0, + total_cleaned_up: 0, + }, + params: {}, + }); + } catch (e) { + logger.error(`Error scheduling ${TASK_ID}, received ${e.message}`); + } +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/find_and_cleanup_tasks.test.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/find_and_cleanup_tasks.test.ts new file mode 100644 index 000000000000..81c2a348bc09 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/find_and_cleanup_tasks.test.ts @@ -0,0 +1,167 @@ +/* + * 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 { CoreStart } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ActionsConfig } from '../config'; +import { ActionsPluginsStart } from '../plugin'; +import { spacesMock } from '../../../spaces/server/mocks'; +import { esKuery } from '../../../../../src/plugins/data/server'; +import { + loggingSystemMock, + savedObjectsRepositoryMock, + savedObjectsServiceMock, + elasticsearchServiceMock, +} from '../../../../../src/core/server/mocks'; +import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { FindAndCleanupTasksOpts, findAndCleanupTasks } from './find_and_cleanup_tasks'; + +jest.mock('./cleanup_tasks', () => ({ + cleanupTasks: jest.fn(), +})); + +describe('findAndCleanupTasks', () => { + const logger = loggingSystemMock.create().get(); + const actionTypeRegistry = actionTypeRegistryMock.create(); + const savedObjectsRepository = savedObjectsRepositoryMock.create(); + const esStart = elasticsearchServiceMock.createStart(); + const spaces = spacesMock.createStart(); + const soService = savedObjectsServiceMock.createStartContract(); + const coreStartServices = (Promise.resolve([ + { + savedObjects: { + ...soService, + createInternalRepository: () => savedObjectsRepository, + }, + elasticsearch: esStart, + }, + { + spaces, + }, + {}, + ]) as unknown) as Promise<[CoreStart, ActionsPluginsStart, unknown]>; + + const config: ActionsConfig['cleanupFailedExecutionsTask'] = { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }; + + const findAndCleanupTasksOpts: FindAndCleanupTasksOpts = { + logger, + actionTypeRegistry, + coreStartServices, + config, + kibanaIndex: '.kibana', + taskManagerIndex: '.kibana_task_manager', + }; + + beforeEach(() => { + actionTypeRegistry.list.mockReturnValue([ + { + id: 'my-action-type', + name: 'My action type', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, + ]); + jest.requireMock('./cleanup_tasks').cleanupTasks.mockResolvedValue({ + success: true, + successCount: 0, + failureCount: 0, + }); + savedObjectsRepository.find.mockResolvedValue({ + total: 0, + page: 1, + per_page: 10, + saved_objects: [], + }); + }); + + it('should call the find function with proper parameters', async () => { + await findAndCleanupTasks(findAndCleanupTasksOpts); + expect(savedObjectsRepository.find).toHaveBeenCalledWith({ + type: 'task', + filter: expect.any(Object), + page: 1, + perPage: config.pageSize, + sortField: 'runAt', + sortOrder: 'asc', + }); + expect(esKuery.toElasticsearchQuery(savedObjectsRepository.find.mock.calls[0][0].filter)) + .toMatchInlineSnapshot(` + Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "task.attributes.status": "failed", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match": Object { + "task.attributes.taskType": "actions:my-action-type", + }, + }, + ], + }, + }, + ], + }, + } + `); + }); + + it('should call the cleanupTasks function with proper parameters', async () => { + await findAndCleanupTasks(findAndCleanupTasksOpts); + expect(jest.requireMock('./cleanup_tasks').cleanupTasks).toHaveBeenCalledWith({ + logger: findAndCleanupTasksOpts.logger, + esClient: esStart.client.asInternalUser, + spaces, + kibanaIndex: findAndCleanupTasksOpts.kibanaIndex, + taskManagerIndex: findAndCleanupTasksOpts.taskManagerIndex, + savedObjectsSerializer: soService.createSerializer(), + tasks: [], + }); + }); + + it('should return the cleanup result', async () => { + const result = await findAndCleanupTasks(findAndCleanupTasksOpts); + expect(result).toEqual({ + success: true, + successCount: 0, + failureCount: 0, + remaining: 0, + }); + }); + + it('should log a message before cleaning up tasks', async () => { + await findAndCleanupTasks(findAndCleanupTasksOpts); + expect(logger.debug).toHaveBeenCalledWith('Removing 0 of 0 failed execution task(s)'); + }); + + it('should log a message after cleaning up tasks', async () => { + await findAndCleanupTasks(findAndCleanupTasksOpts); + expect(logger.debug).toHaveBeenCalledWith( + 'Finished cleanup of failed executions. [success=0, failures=0]' + ); + }); +}); diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/find_and_cleanup_tasks.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/find_and_cleanup_tasks.ts new file mode 100644 index 000000000000..0afb82a515b7 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/find_and_cleanup_tasks.ts @@ -0,0 +1,80 @@ +/* + * 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 { Logger, CoreStart } from 'kibana/server'; +import { ActionsConfig } from '../config'; +import { ActionsPluginsStart } from '../plugin'; +import { ActionTypeRegistryContract } from '../types'; +import { cleanupTasks, CleanupTasksResult } from './cleanup_tasks'; +import { TaskInstance } from '../../../task_manager/server'; +import { nodeBuilder } from '../../../../../src/plugins/data/common'; + +export interface FindAndCleanupTasksOpts { + logger: Logger; + actionTypeRegistry: ActionTypeRegistryContract; + coreStartServices: Promise<[CoreStart, ActionsPluginsStart, unknown]>; + config: ActionsConfig['cleanupFailedExecutionsTask']; + kibanaIndex: string; + taskManagerIndex: string; +} + +export interface FindAndCleanupTasksResult extends CleanupTasksResult { + remaining: number; +} + +export async function findAndCleanupTasks({ + logger, + actionTypeRegistry, + coreStartServices, + config, + kibanaIndex, + taskManagerIndex, +}: FindAndCleanupTasksOpts): Promise { + logger.debug('Starting cleanup of failed executions'); + const [{ savedObjects, elasticsearch }, { spaces }] = await coreStartServices; + const esClient = elasticsearch.client.asInternalUser; + const savedObjectsClient = savedObjects.createInternalRepository(['task']); + const savedObjectsSerializer = savedObjects.createSerializer(); + + const result = await savedObjectsClient.find({ + type: 'task', + filter: nodeBuilder.and([ + nodeBuilder.is('task.attributes.status', 'failed'), + nodeBuilder.or( + actionTypeRegistry + .list() + .map((actionType) => + nodeBuilder.is('task.attributes.taskType', `actions:${actionType.id}`) + ) + ), + ]), + page: 1, + perPage: config.pageSize, + sortField: 'runAt', + sortOrder: 'asc', + }); + + logger.debug( + `Removing ${result.saved_objects.length} of ${result.total} failed execution task(s)` + ); + const cleanupResult = await cleanupTasks({ + logger, + esClient, + spaces, + kibanaIndex, + taskManagerIndex, + savedObjectsSerializer, + tasks: result.saved_objects, + }); + logger.debug( + `Finished cleanup of failed executions. [success=${cleanupResult.successCount}, failures=${cleanupResult.failureCount}]` + ); + return { + ...cleanupResult, + remaining: result.total - cleanupResult.successCount, + }; +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/index.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/index.ts new file mode 100644 index 000000000000..e8e93caed4f8 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { ensureScheduled as ensureCleanupFailedExecutionsTaskScheduled } from './ensure_scheduled'; +export { registerTaskDefinition as registerCleanupFailedExecutionsTaskDefinition } from './register_task_definition'; diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/lib/bulk_delete.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/bulk_delete.ts new file mode 100644 index 000000000000..2e0037d01943 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/bulk_delete.ts @@ -0,0 +1,25 @@ +/* + * 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 { ElasticsearchClient } from 'kibana/server'; +import { ApiResponse, estypes } from '@elastic/elasticsearch'; + +export async function bulkDelete( + esClient: ElasticsearchClient, + index: string, + ids: string[] +): Promise | undefined> { + if (ids.length === 0) { + return; + } + + return await esClient.bulk({ + body: ids.map((id) => ({ + delete: { _index: index, _id: id }, + })), + }); +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/lib/extract_bulk_response_delete_failures.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/extract_bulk_response_delete_failures.ts new file mode 100644 index 000000000000..90418c9763a4 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/extract_bulk_response_delete_failures.ts @@ -0,0 +1,29 @@ +/* + * 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 { ApiResponse, estypes } from '@elastic/elasticsearch'; + +type ResponseFailures = Array>; + +export function extractBulkResponseDeleteFailures( + response: ApiResponse +): ResponseFailures { + const result: ResponseFailures = []; + for (const item of response.body.items) { + if (!item.delete || !item.delete.error) { + continue; + } + + result.push({ + _id: item.delete._id, + status: item.delete.status, + result: item.delete.result, + }); + } + + return result; +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/lib/get_raw_action_task_params_id_from_task.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/get_raw_action_task_params_id_from_task.ts new file mode 100644 index 000000000000..7a9b664387ff --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/get_raw_action_task_params_id_from_task.ts @@ -0,0 +1,27 @@ +/* + * 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 { SavedObjectsFindResult, SavedObjectsSerializer } from 'kibana/server'; +import { spaceIdToNamespace } from '../../lib'; +import { TaskInstance } from '../../../../task_manager/server'; +import { SpacesPluginStart } from '../../../../spaces/server'; + +interface GetRawActionTaskParamsIdFromTaskOpts { + task: SavedObjectsFindResult; + spaces?: SpacesPluginStart; + savedObjectsSerializer: SavedObjectsSerializer; +} + +export function getRawActionTaskParamsIdFromTask({ + task, + spaces, + savedObjectsSerializer, +}: GetRawActionTaskParamsIdFromTaskOpts) { + const { spaceId, actionTaskParamsId } = task.attributes.params; + const namespace = spaceIdToNamespace(spaces, spaceId); + return savedObjectsSerializer.generateRawId(namespace, 'action_task_params', actionTaskParamsId); +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/lib/index.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/index.ts new file mode 100644 index 000000000000..d332c2e1ef06 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/lib/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export { extractBulkResponseDeleteFailures } from './extract_bulk_response_delete_failures'; +export { bulkDelete } from './bulk_delete'; +export { getRawActionTaskParamsIdFromTask } from './get_raw_action_task_params_id_from_task'; diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/register_task_definition.test.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/register_task_definition.test.ts new file mode 100644 index 000000000000..a12ab16facdc --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/register_task_definition.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { CoreStart } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ActionsConfig } from '../config'; +import { ActionsPluginsStart } from '../plugin'; +import { registerTaskDefinition } from './register_task_definition'; +import { taskManagerMock } from '../../../task_manager/server/mocks'; +import { loggingSystemMock, coreMock } from '../../../../../src/core/server/mocks'; +import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { TaskRunnerOpts } from './task_runner'; + +jest.mock('./task_runner', () => ({ taskRunner: jest.fn() })); + +describe('registerTaskDefinition', () => { + const logger = loggingSystemMock.create().get(); + const taskManager = taskManagerMock.createSetup(); + const actionTypeRegistry = actionTypeRegistryMock.create(); + const coreStartServices = coreMock.createSetup().getStartServices() as Promise< + [CoreStart, ActionsPluginsStart, unknown] + >; + + const config: ActionsConfig['cleanupFailedExecutionsTask'] = { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }; + + const taskRunnerOpts: TaskRunnerOpts = { + logger, + coreStartServices, + actionTypeRegistry, + config, + kibanaIndex: '.kibana', + taskManagerIndex: '.kibana_task_manager', + }; + + beforeEach(() => { + jest.resetAllMocks(); + jest.requireMock('./task_runner').taskRunner.mockReturnValue(jest.fn()); + }); + + it('should call registerTaskDefinitions with proper parameters', () => { + registerTaskDefinition(taskManager, taskRunnerOpts); + expect(taskManager.registerTaskDefinitions).toHaveBeenCalledTimes(1); + expect(taskManager.registerTaskDefinitions.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Object { + "cleanup_failed_action_executions": Object { + "createTaskRunner": [MockFunction], + "title": "Cleanup failed action executions", + }, + }, + ], + ] + `); + }); + + it('should call taskRunner with proper parameters', () => { + registerTaskDefinition(taskManager, taskRunnerOpts); + const { taskRunner } = jest.requireMock('./task_runner'); + expect(taskRunner).toHaveBeenCalledWith(taskRunnerOpts); + }); +}); diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/register_task_definition.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/register_task_definition.ts new file mode 100644 index 000000000000..c9a6b486a646 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/register_task_definition.ts @@ -0,0 +1,22 @@ +/* + * 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 { TASK_TYPE } from './constants'; +import { taskRunner, TaskRunnerOpts } from './task_runner'; +import { TaskManagerSetupContract } from '../../../task_manager/server'; + +export function registerTaskDefinition( + taskManager: TaskManagerSetupContract, + taskRunnerOpts: TaskRunnerOpts +) { + taskManager.registerTaskDefinitions({ + [TASK_TYPE]: { + title: 'Cleanup failed action executions', + createTaskRunner: taskRunner(taskRunnerOpts), + }, + }); +} diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/task_runner.test.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/task_runner.test.ts new file mode 100644 index 000000000000..d465e532b028 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/task_runner.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { CoreStart } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import { ActionsConfig } from '../config'; +import { ActionsPluginsStart } from '../plugin'; +import { ConcreteTaskInstance, TaskStatus } from '../../../task_manager/server'; +import { loggingSystemMock, coreMock } from '../../../../../src/core/server/mocks'; +import { actionTypeRegistryMock } from '../action_type_registry.mock'; +import { taskRunner, TaskRunnerOpts } from './task_runner'; + +jest.mock('./find_and_cleanup_tasks', () => ({ + findAndCleanupTasks: jest.fn(), +})); + +describe('taskRunner', () => { + const logger = loggingSystemMock.create().get(); + const actionTypeRegistry = actionTypeRegistryMock.create(); + const coreStartServices = coreMock.createSetup().getStartServices() as Promise< + [CoreStart, ActionsPluginsStart, unknown] + >; + + const config: ActionsConfig['cleanupFailedExecutionsTask'] = { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }; + + const taskRunnerOpts: TaskRunnerOpts = { + logger, + coreStartServices, + actionTypeRegistry, + config, + kibanaIndex: '.kibana', + taskManagerIndex: '.kibana_task_manager', + }; + + const taskInstance: ConcreteTaskInstance = { + id: '123', + scheduledAt: new Date(), + attempts: 0, + status: TaskStatus.Running, + state: { runs: 0, total_cleaned_up: 0 }, + runAt: new Date(), + startedAt: new Date(), + retryAt: new Date(), + ownerId: '234', + taskType: 'foo', + params: {}, + }; + + beforeEach(() => { + jest.resetAllMocks(); + jest.requireMock('./find_and_cleanup_tasks').findAndCleanupTasks.mockResolvedValue({ + success: true, + successCount: 1, + failureCount: 1, + remaining: 0, + }); + }); + + describe('run', () => { + it('should call findAndCleanupTasks with proper parameters', async () => { + const runner = taskRunner(taskRunnerOpts)({ taskInstance }); + await runner.run(); + expect(jest.requireMock('./find_and_cleanup_tasks').findAndCleanupTasks).toHaveBeenCalledWith( + taskRunnerOpts + ); + }); + + it('should update state to reflect cleanup result', async () => { + const runner = taskRunner(taskRunnerOpts)({ taskInstance }); + const { state } = await runner.run(); + expect(state).toEqual({ + runs: 1, + total_cleaned_up: 1, + }); + }); + + it('should return idle schedule when no remaining tasks to cleanup', async () => { + const runner = taskRunner(taskRunnerOpts)({ taskInstance }); + const { schedule } = await runner.run(); + expect(schedule).toEqual({ + interval: '60m', + }); + }); + + it('should return cleanup schedule when there are some remaining tasks to cleanup', async () => { + jest.requireMock('./find_and_cleanup_tasks').findAndCleanupTasks.mockResolvedValue({ + success: true, + successCount: 1, + failureCount: 1, + remaining: 1, + }); + const runner = taskRunner(taskRunnerOpts)({ taskInstance }); + const { schedule } = await runner.run(); + expect(schedule).toEqual({ + interval: '5m', + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/cleanup_failed_executions/task_runner.ts b/x-pack/plugins/actions/server/cleanup_failed_executions/task_runner.ts new file mode 100644 index 000000000000..38eb672238c7 --- /dev/null +++ b/x-pack/plugins/actions/server/cleanup_failed_executions/task_runner.ts @@ -0,0 +1,45 @@ +/* + * 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 { Logger, CoreStart } from 'kibana/server'; +import { ActionsConfig } from '../config'; +import { RunContext, asInterval } from '../../../task_manager/server'; +import { ActionsPluginsStart } from '../plugin'; +import { ActionTypeRegistryContract } from '../types'; +import { findAndCleanupTasks } from './find_and_cleanup_tasks'; + +export interface TaskRunnerOpts { + logger: Logger; + actionTypeRegistry: ActionTypeRegistryContract; + coreStartServices: Promise<[CoreStart, ActionsPluginsStart, unknown]>; + config: ActionsConfig['cleanupFailedExecutionsTask']; + kibanaIndex: string; + taskManagerIndex: string; +} + +export function taskRunner(opts: TaskRunnerOpts) { + return ({ taskInstance }: RunContext) => { + const { state } = taskInstance; + return { + async run() { + const cleanupResult = await findAndCleanupTasks(opts); + return { + state: { + runs: state.runs + 1, + total_cleaned_up: state.total_cleaned_up + cleanupResult.successCount, + }, + schedule: { + interval: + cleanupResult.remaining > 0 + ? asInterval(opts.config.cleanupInterval.asMilliseconds()) + : asInterval(opts.config.idleInterval.asMilliseconds()), + }, + }; + }, + }; + }; +} diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index ad598bffe04b..092b5d2cce58 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -23,6 +23,12 @@ describe('config validation', () => { "allowedHosts": Array [ "*", ], + "cleanupFailedExecutionsTask": Object { + "cleanupInterval": "PT5M", + "enabled": true, + "idleInterval": "PT1H", + "pageSize": 100, + }, "enabled": true, "enabledActionTypes": Array [ "*", @@ -58,6 +64,12 @@ describe('config validation', () => { "allowedHosts": Array [ "*", ], + "cleanupFailedExecutionsTask": Object { + "cleanupInterval": "PT5M", + "enabled": true, + "idleInterval": "PT1H", + "pageSize": 100, + }, "enabled": true, "enabledActionTypes": Array [ "*", diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 36948478816c..7225c54d5759 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -50,6 +50,12 @@ export const configSchema = schema.object({ rejectUnauthorized: schema.boolean({ defaultValue: true }), maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }), responseTimeout: schema.duration({ defaultValue: '60s' }), + cleanupFailedExecutionsTask: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + cleanupInterval: schema.duration({ defaultValue: '5m' }), + idleInterval: schema.duration({ defaultValue: '1h' }), + pageSize: schema.number({ defaultValue: 100 }), + }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/lib/index.ts b/x-pack/plugins/actions/server/lib/index.ts index e900b81bb65a..fba47f9a0f99 100644 --- a/x-pack/plugins/actions/server/lib/index.ts +++ b/x-pack/plugins/actions/server/lib/index.ts @@ -12,6 +12,7 @@ export { ActionExecutor, ActionExecutorContract } from './action_executor'; export { ILicenseState, LicenseState } from './license_state'; export { verifyApiAccess } from './verify_api_access'; export { getActionTypeFeatureUsageName } from './get_action_type_feature_usage_name'; +export { spaceIdToNamespace } from './space_id_to_namespace'; export { ActionTypeDisabledError, ActionTypeDisabledReason, diff --git a/x-pack/plugins/actions/server/lib/space_id_to_namespace.ts b/x-pack/plugins/actions/server/lib/space_id_to_namespace.ts new file mode 100644 index 000000000000..826c4e44b2b8 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/space_id_to_namespace.ts @@ -0,0 +1,12 @@ +/* + * 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 { SpacesPluginStart } from '../../../spaces/server'; + +export function spaceIdToNamespace(spaces?: SpacesPluginStart, spaceId?: string) { + return spaces && spaceId ? spaces.spacesService.spaceIdToNamespace(spaceId) : undefined; +} diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 3485891a0126..9464421d5f0f 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -6,7 +6,7 @@ */ import moment from 'moment'; -import { ByteSizeValue } from '@kbn/config-schema'; +import { schema, ByteSizeValue } from '@kbn/config-schema'; import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; @@ -43,6 +43,12 @@ describe('Actions Plugin', () => { rejectUnauthorized: true, maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration(60000), + cleanupFailedExecutionsTask: { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -207,6 +213,12 @@ describe('Actions Plugin', () => { rejectUnauthorized: true, maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration(60000), + cleanupFailedExecutionsTask: { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -274,6 +286,12 @@ describe('Actions Plugin', () => { rejectUnauthorized: true, maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration('60s'), + cleanupFailedExecutionsTask: { + enabled: true, + cleanupInterval: schema.duration().validate('5m'), + idleInterval: schema.duration().validate('1h'), + pageSize: 100, + }, ...overrides, }; } diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 1d941617789b..106e41259e69 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -26,17 +26,27 @@ import { } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server'; -import { SpacesPluginStart } from '../../spaces/server'; +import { SpacesPluginStart, SpacesPluginSetup } from '../../spaces/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; +import { + ensureCleanupFailedExecutionsTaskScheduled, + registerCleanupFailedExecutionsTaskDefinition, +} from './cleanup_failed_executions'; import { ActionsConfig, getValidatedConfig } from './config'; -import { ActionExecutor, TaskRunnerFactory, LicenseState, ILicenseState } from './lib'; import { ActionsClient } from './actions_client'; import { ActionTypeRegistry } from './action_type_registry'; import { createExecutionEnqueuerFunction } from './create_execute_function'; import { registerBuiltInActionTypes } from './builtin_action_types'; import { registerActionsUsageCollector } from './usage'; +import { + ActionExecutor, + TaskRunnerFactory, + LicenseState, + ILicenseState, + spaceIdToNamespace, +} from './lib'; import { Services, ActionType, @@ -115,6 +125,7 @@ export interface ActionsPluginsSetup { usageCollection?: UsageCollectionSetup; security?: SecurityPluginSetup; features: FeaturesPluginSetup; + spaces?: SpacesPluginSetup; } export interface ActionsPluginsStart { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; @@ -245,6 +256,18 @@ export class ActionsPlugin implements Plugin(), this.licenseState); + // Cleanup failed execution task definition + if (this.actionsConfig.cleanupFailedExecutionsTask.enabled) { + registerCleanupFailedExecutionsTaskDefinition(plugins.taskManager, { + actionTypeRegistry, + logger: this.logger, + coreStartServices: core.getStartServices(), + config: this.actionsConfig.cleanupFailedExecutionsTask, + kibanaIndex: this.kibanaIndexConfig.kibana.index, + taskManagerIndex: plugins.taskManager.index, + }); + } + return { registerType: < Config extends ActionTypeConfig = ActionTypeConfig, @@ -352,18 +375,12 @@ export class ActionsPlugin implements Plugin { - return plugins.spaces && spaceId - ? plugins.spaces.spacesService.spaceIdToNamespace(spaceId) - : undefined; - }; - taskRunnerFactory!.initialize({ logger, actionTypeRegistry: actionTypeRegistry!, encryptedSavedObjectsClient, basePathService: core.http.basePath, - spaceIdToNamespace, + spaceIdToNamespace: (spaceId?: string) => spaceIdToNamespace(plugins.spaces, spaceId), getUnsecuredSavedObjectsClient: (request: KibanaRequest) => this.getUnsecuredSavedObjectsClient(core.savedObjects, request), }); @@ -377,6 +394,15 @@ export class ActionsPlugin implements Plugin { return this.actionTypeRegistry!.isActionTypeEnabled(id, options); diff --git a/x-pack/plugins/fleet/public/applications/fleet/app.tsx b/x-pack/plugins/fleet/public/applications/fleet/app.tsx index f2eee6228906..4a47d39b7793 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/app.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/app.tsx @@ -83,14 +83,14 @@ export const WithPermissionsAndSetup: React.FC = memo(({ children }) => { if (setupResponse.error) { setInitializationError(setupResponse.error); } - if (setupResponse.data.preconfigurationError) { + if (setupResponse.data?.preconfigurationError) { notifications.toasts.addError(setupResponse.data.preconfigurationError, { title: i18n.translate('xpack.fleet.setup.uiPreconfigurationErrorTitle', { defaultMessage: 'Configuration error', }), }); } - if (setupResponse.data.nonFatalPackageUpgradeErrors) { + if (setupResponse.data?.nonFatalPackageUpgradeErrors) { notifications.toasts.addError(setupResponse.data.nonFatalPackageUpgradeErrors, { title: i18n.translate('xpack.fleet.setup.nonFatalPackageErrorsTitle', { defaultMessage: 'One or more packages could not be successfully upgraded', diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index a6d7acccfb4f..627f628f7b9f 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -46,13 +46,10 @@ export const fleetSetupHandler: RequestHandler = async (context, request, respon try { const soClient = context.core.savedObjects.client; const esClient = context.core.elasticsearch.client.asCurrentUser; - const setupStatus = await setupIngestManager(soClient, esClient); - const body: PostIngestSetupResponse = { - isInitialized: true, - }; + const body: PostIngestSetupResponse = await setupIngestManager(soClient, esClient); - if (setupStatus.nonFatalPackageUpgradeErrors.length > 0) { - body.nonFatalPackageUpgradeErrors = setupStatus.nonFatalPackageUpgradeErrors; + if (body.nonFatalPackageUpgradeErrors?.length === 0) { + delete body.nonFatalPackageUpgradeErrors; } return response.ok({ diff --git a/x-pack/plugins/security_solution/.gitattributes b/x-pack/plugins/security_solution/.gitattributes deleted file mode 100644 index 431f25be5e78..000000000000 --- a/x-pack/plugins/security_solution/.gitattributes +++ /dev/null @@ -1,6 +0,0 @@ -# Auto-collapse generated files in GitHub -# https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github -x-pack/plugins/security_solution/server/graphql/types.ts linguist-generated=true -x-pack/plugins/security_solution/public/graphql/types.ts linguist-generated=true -x-pack/plugins/security_solution/public/graphql/introspection.json linguist-generated=true - diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 211e6986e19a..f35974d84164 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -7,7 +7,6 @@ "scripts": { "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/detections/mitre/mitre_tactics_techniques.ts --fix", "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ./server/utils/beat_schema/fields.ts --fix", - "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "../../../node_modules/.bin/cypress open --config-file ./cypress/cypress.json", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", "cypress:run": "../../../node_modules/.bin/cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", diff --git a/x-pack/plugins/security_solution/server/lib/framework/types.ts b/x-pack/plugins/security_solution/server/lib/framework/types.ts index 34012f8f15d1..6665468a2712 100644 --- a/x-pack/plugins/security_solution/server/lib/framework/types.ts +++ b/x-pack/plugins/security_solution/server/lib/framework/types.ts @@ -20,8 +20,6 @@ import { } from '../../../common/search_strategy'; import { SourceConfiguration } from '../sources'; -export * from '../../utils/typed_resolvers'; - export const internalFrameworkRequest = Symbol('internalFrameworkRequest'); export interface FrameworkAdapter { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 2b5a25ec1b31..d0b7e6500c42 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -174,7 +174,6 @@ export class Plugin implements IPlugin { @@ -66,163 +65,6 @@ export const registerCollector: RegisterCollector = ({ }, }, detectionMetrics: { - detection_rules: { - detection_rule_usage: { - query: { - enabled: { type: 'long', _meta: { description: 'Number of query rules enabled' } }, - disabled: { type: 'long', _meta: { description: 'Number of query rules disabled' } }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by query rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to query detection rule alerts' }, - }, - }, - threshold: { - enabled: { - type: 'long', - _meta: { description: 'Number of threshold rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of threshold rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by threshold rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to threshold detection rule alerts', - }, - }, - }, - eql: { - enabled: { type: 'long', _meta: { description: 'Number of eql rules enabled' } }, - disabled: { type: 'long', _meta: { description: 'Number of eql rules disabled' } }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by eql rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to eql detection rule alerts' }, - }, - }, - machine_learning: { - enabled: { - type: 'long', - _meta: { description: 'Number of machine_learning rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of machine_learning rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by machine_learning rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to machine_learning detection rule alerts', - }, - }, - }, - threat_match: { - enabled: { - type: 'long', - _meta: { description: 'Number of threat_match rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'Number of threat_match rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by threat_match rules' }, - }, - cases: { - type: 'long', - _meta: { - description: 'Number of cases attached to threat_match detection rule alerts', - }, - }, - }, - elastic_total: { - enabled: { type: 'long', _meta: { description: 'Number of elastic rules enabled' } }, - disabled: { - type: 'long', - _meta: { description: 'Number of elastic rules disabled' }, - }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by elastic rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to elastic detection rule alerts' }, - }, - }, - custom_total: { - enabled: { type: 'long', _meta: { description: 'Number of custom rules enabled' } }, - disabled: { type: 'long', _meta: { description: 'Number of custom rules disabled' } }, - alerts: { - type: 'long', - _meta: { description: 'Number of alerts generated by custom rules' }, - }, - cases: { - type: 'long', - _meta: { description: 'Number of cases attached to custom detection rule alerts' }, - }, - }, - }, - detection_rule_detail: { - type: 'array', - items: { - rule_name: { - type: 'keyword', - _meta: { description: 'The name of the detection rule' }, - }, - rule_id: { - type: 'keyword', - _meta: { description: 'The UUID id of the detection rule' }, - }, - rule_type: { - type: 'keyword', - _meta: { description: 'The type of detection rule. ie eql, query...' }, - }, - rule_version: { type: 'long', _meta: { description: 'The version of the rule' } }, - enabled: { - type: 'boolean', - _meta: { description: 'If the detection rule has been enabled by the user' }, - }, - elastic_rule: { - type: 'boolean', - _meta: { description: 'If the detection rule has been authored by Elastic' }, - }, - created_on: { - type: 'keyword', - _meta: { description: 'When the detection rule was created on the cluster' }, - }, - updated_on: { - type: 'keyword', - _meta: { description: 'When the detection rule was updated on the cluster' }, - }, - alert_count_daily: { - type: 'long', - _meta: { description: 'The number of daily alerts generated by a rule' }, - }, - cases_count_daily: { - type: 'long', - _meta: { description: 'The number of daily cases generated by a rule' }, - }, - }, - }, - }, ml_jobs: { type: 'array', items: { @@ -290,13 +132,13 @@ export const registerCollector: RegisterCollector = ({ }, }, }, - isReady: () => true, + isReady: () => kibanaIndex.length > 0, fetch: async ({ esClient }: CollectorFetchContext): Promise => { const internalSavedObjectsClient = await getInternalSavedObjectsClient(core); const savedObjectsClient = (internalSavedObjectsClient as unknown) as SavedObjectsClientContract; const [detections, detectionMetrics, endpoints] = await Promise.allSettled([ fetchDetectionsUsage(kibanaIndex, esClient, ml, savedObjectsClient), - fetchDetectionsMetrics(kibanaIndex, signalsIndex, esClient, ml, savedObjectsClient), + fetchDetectionsMetrics(ml, savedObjectsClient), getEndpointTelemetryFromFleet(savedObjectsClient, endpointAppContext, esClient), ]); diff --git a/x-pack/plugins/security_solution/server/usage/detections/dectections_metrics_helpers.test.ts b/x-pack/plugins/security_solution/server/usage/detections/dectections_metrics_helpers.test.ts deleted file mode 100644 index bd470ccabbfe..000000000000 --- a/x-pack/plugins/security_solution/server/usage/detections/dectections_metrics_helpers.test.ts +++ /dev/null @@ -1,147 +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 { initialDetectionRulesUsage, updateDetectionRuleUsage } from './detections_metrics_helpers'; -import { DetectionRuleMetric, DetectionRulesTypeUsage } from './index'; -import { v4 as uuid } from 'uuid'; - -const createStubRule = ( - ruleType: string, - enabled: boolean, - elasticRule: boolean, - alertCount: number, - caseCount: number -): DetectionRuleMetric => ({ - rule_name: uuid(), - rule_id: uuid(), - rule_type: ruleType, - enabled, - elastic_rule: elasticRule, - created_on: uuid(), - updated_on: uuid(), - alert_count_daily: alertCount, - cases_count_daily: caseCount, -}); - -describe('Detections Usage and Metrics', () => { - describe('Update metrics with rule information', () => { - it('Should update elastic and eql rule metric total', async () => { - const initialUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage; - const stubRule = createStubRule('eql', true, true, 1, 1); - const usage = updateDetectionRuleUsage(stubRule, initialUsage); - - expect(usage).toEqual( - expect.objectContaining({ - custom_total: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - elastic_total: { - alerts: 1, - cases: 1, - disabled: 0, - enabled: 1, - }, - eql: { - alerts: 1, - cases: 1, - disabled: 0, - enabled: 1, - }, - machine_learning: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - query: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - threat_match: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - threshold: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - }) - ); - }); - - it('Should update based on multiple metrics', async () => { - const initialUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage; - const stubEqlRule = createStubRule('eql', true, true, 1, 1); - const stubQueryRuleOne = createStubRule('query', true, true, 5, 2); - const stubQueryRuleTwo = createStubRule('query', true, false, 5, 2); - const stubMachineLearningOne = createStubRule('machine_learning', false, false, 0, 10); - const stubMachineLearningTwo = createStubRule('machine_learning', true, true, 22, 44); - - let usage = updateDetectionRuleUsage(stubEqlRule, initialUsage); - usage = updateDetectionRuleUsage(stubQueryRuleOne, usage); - usage = updateDetectionRuleUsage(stubQueryRuleTwo, usage); - usage = updateDetectionRuleUsage(stubMachineLearningOne, usage); - usage = updateDetectionRuleUsage(stubMachineLearningTwo, usage); - - expect(usage).toEqual( - expect.objectContaining({ - custom_total: { - alerts: 5, - cases: 12, - disabled: 1, - enabled: 1, - }, - elastic_total: { - alerts: 28, - cases: 47, - disabled: 0, - enabled: 3, - }, - eql: { - alerts: 1, - cases: 1, - disabled: 0, - enabled: 1, - }, - machine_learning: { - alerts: 22, - cases: 54, - disabled: 1, - enabled: 1, - }, - query: { - alerts: 10, - cases: 4, - disabled: 0, - enabled: 2, - }, - threat_match: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - threshold: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - }) - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_telemetry_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_telemetry_helpers.ts deleted file mode 100644 index bc1e734e4cc3..000000000000 --- a/x-pack/plugins/security_solution/server/usage/detections/detection_telemetry_helpers.ts +++ /dev/null @@ -1,46 +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 { INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; - -export const isElasticRule = (tags: string[] = []) => - tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); - -interface RuleSearchBody { - query: { - bool: { - filter: { - term: { [key: string]: string }; - }; - }; - }; -} - -export interface RuleSearchParams { - body: RuleSearchBody; - filterPath: string[]; - ignoreUnavailable: boolean; - index: string; - size: number; -} - -export interface RuleSearchResult { - alert: { - name: string; - enabled: boolean; - tags: string[]; - createdAt: string; - updatedAt: string; - params: DetectionRuleParms; - }; -} - -interface DetectionRuleParms { - ruleId: string; - version: string; - type: string; -} diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts index f90841ff4e59..f7fa59958aba 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts @@ -302,179 +302,3 @@ export const getMockMlDatafeedStatsResponse = () => ({ }, ], }); - -export const getMockRuleSearchResponse = (immutableTag: string = '__internal_immutable:true') => ({ - took: 2, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 1093, - relation: 'eq', - }, - max_score: 0, - hits: [ - { - _index: '.kibanaindex', - _id: 'alert:6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', - _score: 0, - _source: { - alert: { - name: 'Azure Diagnostic Settings Deletion', - tags: [ - 'Elastic', - 'Cloud', - 'Azure', - 'Continuous Monitoring', - 'SecOps', - 'Monitoring', - '__internal_rule_id:5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de', - `${immutableTag}`, - ], - alertTypeId: 'siem.signals', - consumer: 'siem', - params: { - author: ['Elastic'], - description: - 'Identifies the deletion of diagnostic settings in Azure, which send platform logs and metrics to different destinations. An adversary may delete diagnostic settings in an attempt to evade defenses.', - ruleId: '5370d4cd-2bb3-4d71-abf5-1e1d0ff5a2de', - index: ['filebeat-*', 'logs-azure*'], - falsePositives: [ - 'Deletion of diagnostic settings may be done by a system or network administrator. Verify whether the username, hostname, and/or resource name should be making changes in your environment. Diagnostic settings deletion from unfamiliar users or hosts should be investigated. If known behavior is causing false positives, it can be exempted from the rule.', - ], - from: 'now-25m', - immutable: true, - query: - 'event.dataset:azure.activitylogs and azure.activitylogs.operation_name:"MICROSOFT.INSIGHTS/DIAGNOSTICSETTINGS/DELETE" and event.outcome:(Success or success)', - language: 'kuery', - license: 'Elastic License v2', - outputIndex: '.siem-signals', - maxSignals: 100, - riskScore: 47, - timestampOverride: 'event.ingested', - to: 'now', - type: 'query', - references: [ - 'https://docs.microsoft.com/en-us/azure/azure-monitor/platform/diagnostic-settings', - ], - note: 'The Azure Filebeat module must be enabled to use this rule.', - version: 4, - exceptionsList: [], - }, - schedule: { - interval: '5m', - }, - enabled: false, - actions: [], - throttle: null, - notifyWhen: 'onActiveAlert', - apiKeyOwner: null, - apiKey: null, - createdBy: 'user', - updatedBy: 'user', - createdAt: '2021-03-23T17:15:59.634Z', - updatedAt: '2021-03-23T17:15:59.634Z', - muteAll: false, - mutedInstanceIds: [], - executionStatus: { - status: 'pending', - lastExecutionDate: '2021-03-23T17:15:59.634Z', - error: null, - }, - meta: { - versionApiKeyLastmodified: '8.0.0', - }, - }, - type: 'alert', - references: [], - migrationVersion: { - alert: '7.13.0', - }, - coreMigrationVersion: '8.0.0', - updated_at: '2021-03-23T17:15:59.634Z', - }, - }, - ], - }, -}); - -export const getMockRuleAlertsResponse = (docCount: number) => ({ - took: 7, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 7322, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - aggregations: { - detectionAlerts: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', - doc_count: docCount, - }, - ], - }, - }, -}); - -export const getMockAlertCasesResponse = () => ({ - page: 1, - per_page: 10000, - total: 4, - saved_objects: [ - { - type: 'cases-comments', - id: '3bb5cc10-9249-11eb-85b7-254c8af1a983', - attributes: { - associationType: 'case', - type: 'alert', - alertId: '54802763917f521249c9f68d0d4be0c26cc538404c26dfed1ae7dcfa94ea2226', - index: '.siem-signals-default-000001', - rule: { - id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', - name: 'Azure Diagnostic Settings Deletion', - }, - created_at: '2021-03-31T17:47:59.449Z', - created_by: { - email: '', - full_name: '', - username: '', - }, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - }, - references: [ - { - type: 'cases', - name: 'associated-cases', - id: '3a3a4fa0-9249-11eb-85b7-254c8af1a983', - }, - ], - migrationVersion: {}, - coreMigrationVersion: '8.0.0', - updated_at: '2021-03-31T17:47:59.818Z', - version: 'WzI3MDIyODMsNF0=', - namespaces: ['default'], - score: 0, - }, - ], -}); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index f2a96393aa89..64a33068ad68 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -5,11 +5,8 @@ * 2.0. */ -import { ElasticsearchClient } from '../../../../../../src/core/server'; -import { - elasticsearchServiceMock, - savedObjectsClientMock, -} from '../../../../../../src/core/server/mocks'; +import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; import { mlServicesMock } from '../../lib/machine_learning/mocks'; import { getMockJobSummaryResponse, @@ -18,16 +15,12 @@ import { getMockMlJobDetailsResponse, getMockMlJobStatsResponse, getMockMlDatafeedStatsResponse, - getMockRuleSearchResponse, - getMockRuleAlertsResponse, - getMockAlertCasesResponse, } from './detections.mocks'; import { fetchDetectionsUsage, fetchDetectionsMetrics } from './index'; -const savedObjectsClient = savedObjectsClientMock.create(); - describe('Detections Usage and Metrics', () => { let esClientMock: jest.Mocked; + let savedObjectsClientMock: jest.Mocked; let mlMock: ReturnType; describe('fetchDetectionsUsage()', () => { @@ -37,7 +30,7 @@ describe('Detections Usage and Metrics', () => { }); it('returns zeroed counts if both calls are empty', async () => { - const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient); + const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClientMock); expect(result).toEqual({ detection_rules: { @@ -66,7 +59,7 @@ describe('Detections Usage and Metrics', () => { it('tallies rules data given rules results', async () => { (esClientMock.search as jest.Mock).mockResolvedValue({ body: getMockRulesResponse() }); - const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient); + const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClientMock); expect(result).toEqual( expect.objectContaining({ @@ -94,7 +87,7 @@ describe('Detections Usage and Metrics', () => { jobsSummary: mockJobSummary, }); - const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient); + const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClientMock); expect(result).toEqual( expect.objectContaining({ @@ -113,285 +106,8 @@ describe('Detections Usage and Metrics', () => { }); }); - describe('getDetectionRuleMetrics()', () => { - beforeEach(() => { - esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; - mlMock = mlServicesMock.createSetupContract(); - }); - - it('returns zeroed counts if calls are empty', async () => { - const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual( - expect.objectContaining({ - detection_rules: { - detection_rule_detail: [], - detection_rule_usage: { - query: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - threshold: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - eql: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - machine_learning: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - threat_match: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - elastic_total: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - custom_total: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - }, - }, - ml_jobs: [], - }) - ); - }); - - it('returns information with rule, alerts and cases', async () => { - (esClientMock.search as jest.Mock) - .mockReturnValueOnce({ body: getMockRuleSearchResponse() }) - .mockReturnValue({ body: getMockRuleAlertsResponse(3400) }); - (savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse()); - - const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual( - expect.objectContaining({ - detection_rules: { - detection_rule_detail: [ - { - alert_count_daily: 3400, - cases_count_daily: 1, - created_on: '2021-03-23T17:15:59.634Z', - elastic_rule: true, - enabled: false, - rule_id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', - rule_name: 'Azure Diagnostic Settings Deletion', - rule_type: 'query', - rule_version: 4, - updated_on: '2021-03-23T17:15:59.634Z', - }, - ], - detection_rule_usage: { - custom_total: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - elastic_total: { - alerts: 3400, - cases: 1, - disabled: 1, - enabled: 0, - }, - eql: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - machine_learning: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - query: { - alerts: 3400, - cases: 1, - disabled: 1, - enabled: 0, - }, - threat_match: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - threshold: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - }, - }, - ml_jobs: [], - }) - ); - }); - - it('returns information with on non elastic prebuilt rule', async () => { - (esClientMock.search as jest.Mock) - .mockReturnValueOnce({ body: getMockRuleSearchResponse('not_immutable') }) - .mockReturnValue({ body: getMockRuleAlertsResponse(800) }); - (savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse()); - - const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual( - expect.objectContaining({ - detection_rules: { - detection_rule_detail: [], // *should not* contain custom detection rule details - detection_rule_usage: { - custom_total: { - alerts: 800, - cases: 1, - disabled: 1, - enabled: 0, - }, - elastic_total: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - eql: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - machine_learning: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - query: { - alerts: 800, - cases: 1, - disabled: 1, - enabled: 0, - }, - threat_match: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - threshold: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - }, - }, - ml_jobs: [], - }) - ); - }); - - it('returns information with rule, no alerts and no cases', async () => { - (esClientMock.search as jest.Mock) - .mockReturnValueOnce({ body: getMockRuleSearchResponse() }) - .mockReturnValue({ body: getMockRuleAlertsResponse(0) }); - (savedObjectsClient.find as jest.Mock).mockReturnValue(getMockAlertCasesResponse()); - - const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual( - expect.objectContaining({ - detection_rules: { - detection_rule_detail: [ - { - alert_count_daily: 0, - cases_count_daily: 1, - created_on: '2021-03-23T17:15:59.634Z', - elastic_rule: true, - enabled: false, - rule_id: '6eecd8c2-8bfb-11eb-afbe-1b7a66309c6d', - rule_name: 'Azure Diagnostic Settings Deletion', - rule_type: 'query', - rule_version: 4, - updated_on: '2021-03-23T17:15:59.634Z', - }, - ], - detection_rule_usage: { - custom_total: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - elastic_total: { - alerts: 0, - cases: 1, - disabled: 1, - enabled: 0, - }, - eql: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - machine_learning: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - query: { - alerts: 0, - cases: 1, - disabled: 1, - enabled: 0, - }, - threat_match: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - threshold: { - alerts: 0, - cases: 0, - disabled: 0, - enabled: 0, - }, - }, - }, - ml_jobs: [], - }) - ); - }); - }); - describe('fetchDetectionsMetrics()', () => { beforeEach(() => { - esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; mlMock = mlServicesMock.createSetupContract(); }); @@ -400,7 +116,7 @@ describe('Detections Usage and Metrics', () => { jobs: null, jobStats: null, } as unknown) as ReturnType); - const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient); + const result = await fetchDetectionsMetrics(mlMock, savedObjectsClientMock); expect(result).toEqual( expect.objectContaining({ @@ -422,7 +138,7 @@ describe('Detections Usage and Metrics', () => { datafeedStats: mockDatafeedStatsResponse, } as unknown) as ReturnType); - const result = await fetchDetectionsMetrics('', '', esClientMock, mlMock, savedObjectsClient); + const result = await fetchDetectionsMetrics(mlMock, savedObjectsClientMock); expect(result).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_usage_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts similarity index 51% rename from x-pack/plugins/security_solution/server/usage/detections/detections_usage_helpers.ts rename to x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts index 3c666d4d2178..211c477027ee 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_usage_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections_helpers.ts @@ -7,21 +7,42 @@ import { ElasticsearchClient, - KibanaRequest, SavedObjectsClientContract, + KibanaRequest, } from '../../../../../../src/core/server'; -import { SIGNALS_ID } from '../../../common/constants'; +import { MlPluginSetup } from '../../../../ml/server'; +import { SIGNALS_ID, INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; +import { DetectionRulesUsage, MlJobsUsage, MlJobMetric } from './index'; import { isJobStarted } from '../../../common/machine_learning/helpers'; import { isSecurityJob } from '../../../common/machine_learning/is_security_job'; -import { MlPluginSetup } from '../../../../ml/server'; -import { DetectionRulesUsage, MlJobsUsage } from './index'; -import { isElasticRule, RuleSearchParams, RuleSearchResult } from './detection_telemetry_helpers'; interface DetectionsMetric { isElastic: boolean; isEnabled: boolean; } +interface RuleSearchBody { + query: { + bool: { + filter: { + term: { [key: string]: string }; + }; + }; + }; +} +interface RuleSearchParams { + body: RuleSearchBody; + filterPath: string[]; + ignoreUnavailable: boolean; + index: string; + size: number; +} +interface RuleSearchResult { + alert: { enabled: boolean; tags: string[] }; +} + +const isElasticRule = (tags: string[]) => tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); + /** * Default detection rule usage count */ @@ -149,6 +170,7 @@ export const getRulesUsage = async ( if (ruleResults.hits?.hits?.length > 0) { rulesUsage = ruleResults.hits.hits.reduce((usage, hit) => { + // @ts-expect-error _source is optional const isElastic = isElasticRule(hit._source?.alert.tags); const isEnabled = Boolean(hit._source?.alert.enabled); @@ -189,3 +211,93 @@ export const getMlJobsUsage = async ( return jobsUsage; }; + +export const getMlJobMetrics = async ( + ml: MlPluginSetup | undefined, + savedObjectClient: SavedObjectsClientContract +): Promise => { + if (ml) { + try { + const fakeRequest = { headers: {} } as KibanaRequest; + const jobsType = 'security'; + const securityJobStats = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .jobStats(jobsType); + + const jobDetails = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .jobs(jobsType); + + const jobDetailsCache = new Map(); + jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail)); + + const datafeedStats = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .datafeedStats(); + + const datafeedStatsCache = new Map(); + datafeedStats.datafeeds.forEach((datafeedStat) => + datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat) + ); + + return securityJobStats.jobs.map((stat) => { + const jobId = stat.job_id; + const jobDetail = jobDetailsCache.get(stat.job_id); + const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`); + + return { + job_id: jobId, + open_time: stat.open_time, + create_time: jobDetail?.create_time, + finished_time: jobDetail?.finished_time, + state: stat.state, + data_counts: { + bucket_count: stat.data_counts.bucket_count, + empty_bucket_count: stat.data_counts.empty_bucket_count, + input_bytes: stat.data_counts.input_bytes, + input_record_count: stat.data_counts.input_record_count, + last_data_time: stat.data_counts.last_data_time, + processed_record_count: stat.data_counts.processed_record_count, + }, + model_size_stats: { + bucket_allocation_failures_count: + stat.model_size_stats.bucket_allocation_failures_count, + memory_status: stat.model_size_stats.memory_status, + model_bytes: stat.model_size_stats.model_bytes, + model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded, + model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit, + peak_model_bytes: stat.model_size_stats.peak_model_bytes, + }, + timing_stats: { + average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms, + bucket_count: stat.timing_stats.bucket_count, + exponential_average_bucket_processing_time_ms: + stat.timing_stats.exponential_average_bucket_processing_time_ms, + exponential_average_bucket_processing_time_per_hour_ms: + stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms, + maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms, + minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms, + total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms, + }, + datafeed: { + datafeed_id: datafeed?.datafeed_id, + state: datafeed?.state, + timing_stats: { + average_search_time_per_bucket_ms: + datafeed?.timing_stats.average_search_time_per_bucket_ms, + bucket_count: datafeed?.timing_stats.bucket_count, + exponential_average_search_time_per_hour_ms: + datafeed?.timing_stats.exponential_average_search_time_per_hour_ms, + search_count: datafeed?.timing_stats.search_count, + total_search_time_ms: datafeed?.timing_stats.total_search_time_ms, + }, + }, + } as MlJobMetric; + }); + } catch (e) { + // ignore failure, usage will be zeroed + } + } + + return []; +}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_metrics_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_metrics_helpers.ts deleted file mode 100644 index 66b52ac5f96a..000000000000 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_metrics_helpers.ts +++ /dev/null @@ -1,380 +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 { - ElasticsearchClient, - KibanaRequest, - SavedObjectsClientContract, -} from '../../../../../../src/core/server'; -import { - AlertsAggregationResponse, - CasesSavedObject, - DetectionRulesTypeUsage, - DetectionRuleMetric, - DetectionRuleAdoption, - MlJobMetric, -} from './index'; -import { SIGNALS_ID } from '../../../common/constants'; -import { DatafeedStats, Job, MlPluginSetup } from '../../../../ml/server'; -import { isElasticRule, RuleSearchParams, RuleSearchResult } from './detection_telemetry_helpers'; - -/** - * Default detection rule usage count, split by type + elastic/custom - */ -export const initialDetectionRulesUsage: DetectionRulesTypeUsage = { - query: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - threshold: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - eql: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - machine_learning: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - threat_match: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - elastic_total: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, - custom_total: { - enabled: 0, - disabled: 0, - alerts: 0, - cases: 0, - }, -}; - -/* eslint-disable complexity */ -export const updateDetectionRuleUsage = ( - detectionRuleMetric: DetectionRuleMetric, - usage: DetectionRulesTypeUsage -): DetectionRulesTypeUsage => { - let updatedUsage = usage; - - if (detectionRuleMetric.rule_type === 'query') { - updatedUsage = { - ...usage, - query: { - ...usage.query, - enabled: detectionRuleMetric.enabled ? usage.query.enabled + 1 : usage.query.enabled, - disabled: !detectionRuleMetric.enabled ? usage.query.disabled + 1 : usage.query.disabled, - alerts: usage.query.alerts + detectionRuleMetric.alert_count_daily, - cases: usage.query.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } else if (detectionRuleMetric.rule_type === 'threshold') { - updatedUsage = { - ...usage, - threshold: { - ...usage.threshold, - enabled: detectionRuleMetric.enabled - ? usage.threshold.enabled + 1 - : usage.threshold.enabled, - disabled: !detectionRuleMetric.enabled - ? usage.threshold.disabled + 1 - : usage.threshold.disabled, - alerts: usage.threshold.alerts + detectionRuleMetric.alert_count_daily, - cases: usage.threshold.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } else if (detectionRuleMetric.rule_type === 'eql') { - updatedUsage = { - ...usage, - eql: { - ...usage.eql, - enabled: detectionRuleMetric.enabled ? usage.eql.enabled + 1 : usage.eql.enabled, - disabled: !detectionRuleMetric.enabled ? usage.eql.disabled + 1 : usage.eql.disabled, - alerts: usage.eql.alerts + detectionRuleMetric.alert_count_daily, - cases: usage.eql.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } else if (detectionRuleMetric.rule_type === 'machine_learning') { - updatedUsage = { - ...usage, - machine_learning: { - ...usage.machine_learning, - enabled: detectionRuleMetric.enabled - ? usage.machine_learning.enabled + 1 - : usage.machine_learning.enabled, - disabled: !detectionRuleMetric.enabled - ? usage.machine_learning.disabled + 1 - : usage.machine_learning.disabled, - alerts: usage.machine_learning.alerts + detectionRuleMetric.alert_count_daily, - cases: usage.machine_learning.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } else if (detectionRuleMetric.rule_type === 'threat_match') { - updatedUsage = { - ...usage, - threat_match: { - ...usage.threat_match, - enabled: detectionRuleMetric.enabled - ? usage.threat_match.enabled + 1 - : usage.threat_match.enabled, - disabled: !detectionRuleMetric.enabled - ? usage.threat_match.disabled + 1 - : usage.threat_match.disabled, - alerts: usage.threat_match.alerts + detectionRuleMetric.alert_count_daily, - cases: usage.threat_match.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } - - if (detectionRuleMetric.elastic_rule) { - updatedUsage = { - ...updatedUsage, - elastic_total: { - ...updatedUsage.elastic_total, - enabled: detectionRuleMetric.enabled - ? updatedUsage.elastic_total.enabled + 1 - : updatedUsage.elastic_total.enabled, - disabled: !detectionRuleMetric.enabled - ? updatedUsage.elastic_total.disabled + 1 - : updatedUsage.elastic_total.disabled, - alerts: updatedUsage.elastic_total.alerts + detectionRuleMetric.alert_count_daily, - cases: updatedUsage.elastic_total.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } else { - updatedUsage = { - ...updatedUsage, - custom_total: { - ...updatedUsage.custom_total, - enabled: detectionRuleMetric.enabled - ? updatedUsage.custom_total.enabled + 1 - : updatedUsage.custom_total.enabled, - disabled: !detectionRuleMetric.enabled - ? updatedUsage.custom_total.disabled + 1 - : updatedUsage.custom_total.disabled, - alerts: updatedUsage.custom_total.alerts + detectionRuleMetric.alert_count_daily, - cases: updatedUsage.custom_total.cases + detectionRuleMetric.cases_count_daily, - }, - }; - } - - return updatedUsage; -}; - -export const getDetectionRuleMetrics = async ( - kibanaIndex: string, - signalsIndex: string, - esClient: ElasticsearchClient, - savedObjectClient: SavedObjectsClientContract -): Promise => { - let rulesUsage: DetectionRulesTypeUsage = initialDetectionRulesUsage; - const ruleSearchOptions: RuleSearchParams = { - body: { query: { bool: { filter: { term: { 'alert.alertTypeId': SIGNALS_ID } } } } }, - filterPath: [], - ignoreUnavailable: true, - index: kibanaIndex, - size: 1, - }; - - try { - const { body: ruleResults } = await esClient.search(ruleSearchOptions); - const { body: detectionAlertsResp } = (await esClient.search({ - index: `${signalsIndex}*`, - size: 0, - body: { - aggs: { - detectionAlerts: { - terms: { field: 'signal.rule.id.keyword' }, - }, - }, - query: { - bool: { - filter: [ - { - range: { - '@timestamp': { - gte: 'now-24h', - lte: 'now', - }, - }, - }, - ], - }, - }, - }, - })) as { body: AlertsAggregationResponse }; - - const cases = await savedObjectClient.find({ - type: 'cases-comments', - fields: [], - page: 1, - perPage: 10_000, - filter: 'cases-comments.attributes.type: alert', - }); - - const casesCache = cases.saved_objects.reduce((cache, { attributes: casesObject }) => { - const ruleId = casesObject.rule.id; - - const cacheCount = cache.get(ruleId); - if (cacheCount === undefined) { - cache.set(ruleId, 1); - } else { - cache.set(ruleId, cacheCount + 1); - } - return cache; - }, new Map()); - - const alertBuckets = detectionAlertsResp.aggregations?.detectionAlerts?.buckets ?? []; - - const alertsCache = new Map(); - alertBuckets.map((bucket) => alertsCache.set(bucket.key, bucket.doc_count)); - - if (ruleResults.hits?.hits?.length > 0) { - const ruleObjects = ruleResults.hits.hits.map((hit) => { - const ruleId = hit._id.split(':')[1]; - const isElastic = isElasticRule(hit._source?.alert.tags); - return { - rule_name: hit._source?.alert.name, - rule_id: ruleId, - rule_type: hit._source?.alert.params.type, - rule_version: hit._source?.alert.params.version, - enabled: hit._source?.alert.enabled, - elastic_rule: isElastic, - created_on: hit._source?.alert.createdAt, - updated_on: hit._source?.alert.updatedAt, - alert_count_daily: alertsCache.get(ruleId) || 0, - cases_count_daily: casesCache.get(ruleId) || 0, - } as DetectionRuleMetric; - }); - - // Only bring back rule detail on elastic prepackaged detection rules - const elasticRuleObjects = ruleObjects.filter((hit) => hit.elastic_rule === true); - - rulesUsage = ruleObjects.reduce((usage, rule) => { - return updateDetectionRuleUsage(rule, usage); - }, rulesUsage); - - return { - detection_rule_detail: elasticRuleObjects, - detection_rule_usage: rulesUsage, - }; - } - } catch (e) { - // ignore failure, usage will be zeroed - } - - return { - detection_rule_detail: [], - detection_rule_usage: rulesUsage, - }; -}; - -export const getMlJobMetrics = async ( - ml: MlPluginSetup | undefined, - savedObjectClient: SavedObjectsClientContract -): Promise => { - if (ml) { - try { - const fakeRequest = { headers: {} } as KibanaRequest; - const jobsType = 'security'; - const securityJobStats = await ml - .anomalyDetectorsProvider(fakeRequest, savedObjectClient) - .jobStats(jobsType); - - const jobDetails = await ml - .anomalyDetectorsProvider(fakeRequest, savedObjectClient) - .jobs(jobsType); - - const jobDetailsCache = new Map(); - jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail)); - - const datafeedStats = await ml - .anomalyDetectorsProvider(fakeRequest, savedObjectClient) - .datafeedStats(); - - const datafeedStatsCache = new Map(); - datafeedStats.datafeeds.forEach((datafeedStat) => - datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat) - ); - - return securityJobStats.jobs.map((stat) => { - const jobId = stat.job_id; - const jobDetail = jobDetailsCache.get(stat.job_id); - const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`); - - return { - job_id: jobId, - open_time: stat.open_time, - create_time: jobDetail?.create_time, - finished_time: jobDetail?.finished_time, - state: stat.state, - data_counts: { - bucket_count: stat.data_counts.bucket_count, - empty_bucket_count: stat.data_counts.empty_bucket_count, - input_bytes: stat.data_counts.input_bytes, - input_record_count: stat.data_counts.input_record_count, - last_data_time: stat.data_counts.last_data_time, - processed_record_count: stat.data_counts.processed_record_count, - }, - model_size_stats: { - bucket_allocation_failures_count: - stat.model_size_stats.bucket_allocation_failures_count, - memory_status: stat.model_size_stats.memory_status, - model_bytes: stat.model_size_stats.model_bytes, - model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded, - model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit, - peak_model_bytes: stat.model_size_stats.peak_model_bytes, - }, - timing_stats: { - average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms, - bucket_count: stat.timing_stats.bucket_count, - exponential_average_bucket_processing_time_ms: - stat.timing_stats.exponential_average_bucket_processing_time_ms, - exponential_average_bucket_processing_time_per_hour_ms: - stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms, - maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms, - minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms, - total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms, - }, - datafeed: { - datafeed_id: datafeed?.datafeed_id, - state: datafeed?.state, - timing_stats: { - average_search_time_per_bucket_ms: - datafeed?.timing_stats.average_search_time_per_bucket_ms, - bucket_count: datafeed?.timing_stats.bucket_count, - exponential_average_search_time_per_hour_ms: - datafeed?.timing_stats.exponential_average_search_time_per_hour_ms, - search_count: datafeed?.timing_stats.search_count, - total_search_time_ms: datafeed?.timing_stats.total_search_time_ms, - }, - }, - } as MlJobMetric; - }); - } catch (e) { - // ignore failure, usage will be zeroed - } - } - - return []; -}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index dd1cffd06a60..39c8f3159fe0 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -8,15 +8,11 @@ import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; import { getMlJobsUsage, + getMlJobMetrics, getRulesUsage, initialRulesUsage, initialMlJobsUsage, -} from './detections_usage_helpers'; -import { - getMlJobMetrics, - getDetectionRuleMetrics, - initialDetectionRulesUsage, -} from './detections_metrics_helpers'; +} from './detections_helpers'; import { MlPluginSetup } from '../../../../ml/server'; interface FeatureUsage { @@ -24,23 +20,6 @@ interface FeatureUsage { disabled: number; } -interface FeatureTypeUsage { - enabled: number; - disabled: number; - alerts: number; - cases: number; -} - -export interface DetectionRulesTypeUsage { - query: FeatureTypeUsage; - threshold: FeatureTypeUsage; - eql: FeatureTypeUsage; - machine_learning: FeatureTypeUsage; - threat_match: FeatureTypeUsage; - elastic_total: FeatureTypeUsage; - custom_total: FeatureTypeUsage; -} - export interface DetectionRulesUsage { custom: FeatureUsage; elastic: FeatureUsage; @@ -58,7 +37,6 @@ export interface DetectionsUsage { export interface DetectionMetrics { ml_jobs: MlJobMetric[]; - detection_rules: DetectionRuleAdoption; } export interface MlJobDataCount { @@ -98,45 +76,6 @@ export interface MlJobMetric { timing_stats: MlTimingStats; } -export interface DetectionRuleMetric { - rule_name: string; - rule_id: string; - rule_type: string; - enabled: boolean; - elastic_rule: boolean; - created_on: string; - updated_on: string; - alert_count_daily: number; - cases_count_daily: number; -} - -export interface DetectionRuleAdoption { - detection_rule_detail: DetectionRuleMetric[]; - detection_rule_usage: DetectionRulesTypeUsage; -} - -export interface AlertsAggregationResponse { - hits: { - total: { value: number }; - }; - aggregations: { - [aggName: string]: { - buckets: Array<{ key: string; doc_count: number }>; - }; - }; -} - -export interface CasesSavedObject { - associationType: string; - type: string; - alertId: string; - index: string; - rule: { - id: string; - name: string; - }; -} - export const defaultDetectionsUsage = { detection_rules: initialRulesUsage, ml_jobs: initialMlJobsUsage, @@ -160,22 +99,12 @@ export const fetchDetectionsUsage = async ( }; export const fetchDetectionsMetrics = async ( - kibanaIndex: string, - signalsIndex: string, - esClient: ElasticsearchClient, ml: MlPluginSetup | undefined, savedObjectClient: SavedObjectsClientContract ): Promise => { - const [mlJobMetrics, detectionRuleMetrics] = await Promise.allSettled([ - getMlJobMetrics(ml, savedObjectClient), - getDetectionRuleMetrics(kibanaIndex, signalsIndex, esClient, savedObjectClient), - ]); + const [mlJobMetrics] = await Promise.allSettled([getMlJobMetrics(ml, savedObjectClient)]); return { ml_jobs: mlJobMetrics.status === 'fulfilled' ? mlJobMetrics.value : [], - detection_rules: - detectionRuleMetrics.status === 'fulfilled' - ? detectionRuleMetrics.value - : { detection_rule_detail: [], detection_rule_usage: initialDetectionRulesUsage }, }; }; diff --git a/x-pack/plugins/security_solution/server/usage/types.ts b/x-pack/plugins/security_solution/server/usage/types.ts index 4e1e647952a7..c06c8a4722cd 100644 --- a/x-pack/plugins/security_solution/server/usage/types.ts +++ b/x-pack/plugins/security_solution/server/usage/types.ts @@ -11,7 +11,6 @@ import { SetupPlugins } from '../plugin'; export type CollectorDependencies = { kibanaIndex: string; - signalsIndex: string; core: CoreSetup; endpointAppContext: EndpointAppContext; } & Pick; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/field.mock.ts b/x-pack/plugins/security_solution/server/utils/build_query/field.mock.ts deleted file mode 100644 index 3c8d1b4c1d6b..000000000000 --- a/x-pack/plugins/security_solution/server/utils/build_query/field.mock.ts +++ /dev/null @@ -1,172 +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 { FieldNode } from 'graphql'; - -export const mockFields: FieldNode = { - kind: 'Field', - name: { - kind: 'Name', - value: 'Hosts', - }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { - kind: 'Name', - value: 'totalCount', - }, - arguments: [], - directives: [], - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'edges', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { - kind: 'Name', - value: 'host', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { - kind: 'Name', - value: '_id', - }, - arguments: [], - directives: [], - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'name', - }, - arguments: [], - directives: [], - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'os', - }, - arguments: [], - directives: [], - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'version', - }, - arguments: [], - directives: [], - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'firstSeen', - }, - arguments: [], - directives: [], - }, - ], - }, - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'cursor', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { - kind: 'Name', - value: 'value', - }, - arguments: [], - directives: [], - }, - ], - }, - }, - ], - }, - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'pageInfo', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { - kind: 'Name', - value: 'endCursor', - }, - arguments: [], - directives: [], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { - kind: 'Name', - value: 'value', - }, - arguments: [], - directives: [], - }, - ], - }, - }, - { - kind: 'Field', - name: { - kind: 'Name', - value: 'hasNextPage', - }, - arguments: [], - directives: [], - }, - ], - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/fields.test.ts b/x-pack/plugins/security_solution/server/utils/build_query/fields.test.ts deleted file mode 100644 index b34a3f7ed63a..000000000000 --- a/x-pack/plugins/security_solution/server/utils/build_query/fields.test.ts +++ /dev/null @@ -1,27 +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 { mockFields } from './field.mock'; -import { getFields } from './fields'; - -describe('the ConfigurationSourcesAdapter', () => { - test('adds the default source when no sources are configured', async () => { - const expectedData = [ - 'totalCount', - 'edges.host._id', - 'edges.host.name', - 'edges.host.os', - 'edges.host.version', - 'edges.host.firstSeen', - 'edges.cursor.value', - 'pageInfo.endCursor.value', - 'pageInfo.hasNextPage', - ]; - - expect(getFields(mockFields)).toEqual(expectedData); - }); -}); diff --git a/x-pack/plugins/security_solution/server/utils/build_query/fields.ts b/x-pack/plugins/security_solution/server/utils/build_query/fields.ts deleted file mode 100644 index da7fb1e2af81..000000000000 --- a/x-pack/plugins/security_solution/server/utils/build_query/fields.ts +++ /dev/null @@ -1,34 +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 { FieldNode, SelectionNode, SelectionSetNode } from 'graphql'; -import { isEmpty } from 'lodash/fp'; - -export const getFields = ( - data: SelectionSetNode | FieldNode, - fields: string[] = [], - postFields: string[] = [] -): string[] => { - if (data.kind === 'Field' && data.selectionSet && !isEmpty(data.selectionSet.selections)) { - return getFields(data.selectionSet, fields); - } else if (data.kind === 'SelectionSet') { - return data.selections.reduce((res: string[], item: SelectionNode) => { - if (item.kind === 'Field') { - const field: FieldNode = item as FieldNode; - if (field.name.kind === 'Name' && field.name.value.includes('kpi')) { - return [...res, field.name.value]; - } else if (field.selectionSet && !isEmpty(field.selectionSet.selections)) { - return getFields(field.selectionSet, res, postFields.concat(field.name.value)); - } - return [...res, [...postFields, field.name.value].join('.')]; - } - return res; - }, fields as string[]); - } - - return fields; -}; diff --git a/x-pack/plugins/security_solution/server/utils/build_query/index.ts b/x-pack/plugins/security_solution/server/utils/build_query/index.ts index 7e06b6dbaa89..61c4831f7f72 100644 --- a/x-pack/plugins/security_solution/server/utils/build_query/index.ts +++ b/x-pack/plugins/security_solution/server/utils/build_query/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -export * from './fields'; export * from './filters'; export * from './merge_fields_with_hits'; export * from './calculate_timeseries_interval'; diff --git a/x-pack/plugins/security_solution/server/utils/typed_resolvers.ts b/x-pack/plugins/security_solution/server/utils/typed_resolvers.ts deleted file mode 100644 index 96156797892d..000000000000 --- a/x-pack/plugins/security_solution/server/utils/typed_resolvers.ts +++ /dev/null @@ -1,108 +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 { GraphQLResolveInfo } from 'graphql'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type Resolver = ( - parent: Parent, - args: Args, - context: TContext, - info: GraphQLResolveInfo -) => Promise | Result; - -type ResolverResult = R | Promise; - -type AppResolverResult = - | Promise - | Promise<{ [P in keyof R]: () => Promise }> - | { [P in keyof R]: () => Promise } - | { [P in keyof R]: () => R[P] } - | R; - -export type ResultOf = Resolver_ extends Resolver> - ? Result - : never; - -export type SubsetResolverWithFields = R extends Resolver< - Array, - infer ParentInArray, - infer ContextInArray, - infer ArgsInArray -> - ? Resolver< - Array>>, - ParentInArray, - ContextInArray, - ArgsInArray - > - : R extends Resolver - ? Resolver>, Parent, Context, Args> - : never; - -export type SubsetResolverWithoutFields = R extends Resolver< - Array, - infer ParentInArray, - infer ContextInArray, - infer ArgsInArray -> - ? Resolver< - Array>>, - ParentInArray, - ContextInArray, - ArgsInArray - > - : R extends Resolver - ? Resolver>, Parent, Context, Args> - : never; - -export type ResolverWithParent = Resolver_ extends Resolver< - infer Result, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - any, - infer Context, - infer Args -> - ? Resolver - : never; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type AppResolver = Resolver< - AppResolverResult, - Parent, - Context, - Args ->; - -export type AppResolverOf = Resolver_ extends Resolver< - ResolverResult, - never, - infer ContextWithNeverParent, - infer ArgsWithNeverParent -> - ? AppResolver - : Resolver_ extends Resolver< - ResolverResult, - infer Parent, - infer Context, - infer Args - > - ? AppResolver - : never; - -export type AppResolverWithFields = AppResolverOf< - SubsetResolverWithFields ->; - -export type AppResolverWithoutFields = AppResolverOf< - SubsetResolverWithoutFields ->; - -export type ChildResolverOf = ResolverWithParent< - Resolver_, - ResultOf ->; diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index a34f5a87fddb..9d2f8f4189ae 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -20,6 +20,7 @@ export { RunContext, } from './task'; +export { asInterval } from './lib/intervals'; export { isUnrecoverableError, throwUnrecoverableError } from './task_running'; export { diff --git a/x-pack/plugins/task_manager/server/mocks.ts b/x-pack/plugins/task_manager/server/mocks.ts index 3a45cefd9bda..c713e1e98a1e 100644 --- a/x-pack/plugins/task_manager/server/mocks.ts +++ b/x-pack/plugins/task_manager/server/mocks.ts @@ -9,6 +9,7 @@ import { TaskManagerSetupContract, TaskManagerStartContract } from './plugin'; const createSetupMock = () => { const mock: jest.Mocked = { + index: '.kibana_task_manager', addMiddleware: jest.fn(), registerTaskDefinitions: jest.fn(), }; diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index 507a021214a9..51199da26ee7 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -28,10 +28,13 @@ import { TaskScheduling } from './task_scheduling'; import { healthRoute } from './routes'; import { createMonitoringStats, MonitoringStats } from './monitoring'; -export type TaskManagerSetupContract = { addMiddleware: (middleware: Middleware) => void } & Pick< - TaskTypeDictionary, - 'registerTaskDefinitions' ->; +export type TaskManagerSetupContract = { + /** + * @deprecated + */ + index: string; + addMiddleware: (middleware: Middleware) => void; +} & Pick; export type TaskManagerStartContract = Pick< TaskScheduling, @@ -95,6 +98,7 @@ export class TaskManagerPlugin }); return { + index: this.config.index, addMiddleware: (middleware: Middleware) => { this.assertStillInSetup('add Middleware'); this.middleware = addMiddlewareToChain(this.middleware, middleware); diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 29b32cf87aa6..a6bbf076a5b0 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4544,277 +4544,6 @@ }, "detectionMetrics": { "properties": { - "detection_rules": { - "properties": { - "detection_rule_usage": { - "properties": { - "query": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of query rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of query rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by query rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to query detection rule alerts" - } - } - } - }, - "threshold": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of threshold rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of threshold rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by threshold rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to threshold detection rule alerts" - } - } - } - }, - "eql": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of eql rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of eql rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by eql rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to eql detection rule alerts" - } - } - } - }, - "machine_learning": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of machine_learning rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of machine_learning rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by machine_learning rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to machine_learning detection rule alerts" - } - } - } - }, - "threat_match": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of threat_match rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of threat_match rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by threat_match rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to threat_match detection rule alerts" - } - } - } - }, - "elastic_total": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of elastic rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of elastic rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by elastic rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to elastic detection rule alerts" - } - } - } - }, - "custom_total": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "Number of custom rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "Number of custom rules disabled" - } - }, - "alerts": { - "type": "long", - "_meta": { - "description": "Number of alerts generated by custom rules" - } - }, - "cases": { - "type": "long", - "_meta": { - "description": "Number of cases attached to custom detection rule alerts" - } - } - } - } - } - }, - "detection_rule_detail": { - "type": "array", - "items": { - "properties": { - "rule_name": { - "type": "keyword", - "_meta": { - "description": "The name of the detection rule" - } - }, - "rule_id": { - "type": "keyword", - "_meta": { - "description": "The UUID id of the detection rule" - } - }, - "rule_type": { - "type": "keyword", - "_meta": { - "description": "The type of detection rule. ie eql, query..." - } - }, - "rule_version": { - "type": "long", - "_meta": { - "description": "The version of the rule" - } - }, - "enabled": { - "type": "boolean", - "_meta": { - "description": "If the detection rule has been enabled by the user" - } - }, - "elastic_rule": { - "type": "boolean", - "_meta": { - "description": "If the detection rule has been authored by Elastic" - } - }, - "created_on": { - "type": "keyword", - "_meta": { - "description": "When the detection rule was created on the cluster" - } - }, - "updated_on": { - "type": "keyword", - "_meta": { - "description": "When the detection rule was updated on the cluster" - } - }, - "alert_count_daily": { - "type": "long", - "_meta": { - "description": "The number of daily alerts generated by a rule" - } - }, - "cases_count_daily": { - "type": "long", - "_meta": { - "description": "The number of daily cases generated by a rule" - } - } - } - } - } - } - }, "ml_jobs": { "type": "array", "items": { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index bf5d05ee4624..9a7cd8d333b4 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -15,6 +15,7 @@ import { defineActionTypes } from './action_types'; import { defineRoutes } from './routes'; import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; +import { PluginStartContract as ActionsPluginStart } from '../../../../../../../plugins/actions/server'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; @@ -26,6 +27,7 @@ export interface FixtureStartDeps { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security?: SecurityPluginStart; spaces?: SpacesPluginStart; + actions: ActionsPluginStart; } export class FixturePlugin implements Plugin { diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts index 5dc607bdbb69..091034bd1df7 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/routes.ts @@ -5,6 +5,7 @@ * 2.0. */ +import uuid from 'uuid'; import { CoreSetup, RequestHandlerContext, @@ -174,10 +175,10 @@ export function defineRoutes(core: CoreSetup, { logger }: { lo router.put( { - path: '/api/alerts_fixture/{id}/reschedule_task', + path: '/api/alerts_fixture/{taskId}/reschedule_task', validate: { params: schema.object({ - id: schema.string(), + taskId: schema.string(), }), body: schema.object({ runAt: schema.string(), @@ -189,23 +190,20 @@ export function defineRoutes(core: CoreSetup, { logger }: { lo req: KibanaRequest, res: KibanaResponseFactory ): Promise> => { - const { id } = req.params; + const { taskId } = req.params; const { runAt } = req.body; const [{ savedObjects }] = await core.getStartServices(); const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { includedHiddenTypes: ['task', 'alert'], }); - const alert = await savedObjectsWithTasksAndAlerts.get('alert', id); const result = await retryIfConflicts( logger, - `/api/alerts_fixture/${id}/reschedule_task`, + `/api/alerts_fixture/${taskId}/reschedule_task`, async () => { - return await savedObjectsWithTasksAndAlerts.update( - 'task', - alert.attributes.scheduledTaskId!, - { runAt } - ); + return await savedObjectsWithTasksAndAlerts.update('task', taskId, { + runAt, + }); } ); return res.ok({ body: result }); @@ -278,4 +276,53 @@ export function defineRoutes(core: CoreSetup, { logger }: { lo } } ); + + router.post( + { + path: '/api/alerts_fixture/{id}/enqueue_action', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + params: schema.recordOf(schema.string(), schema.any()), + }), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + try { + const [, { actions, security, spaces }] = await core.getStartServices(); + const actionsClient = await actions.getActionsClientWithRequest(req); + + const createAPIKeyResult = + security && + (await security.authc.apiKeys.grantAsInternalUser(req, { + name: `alerts_fixture:enqueue_action:${uuid.v4()}`, + role_descriptors: {}, + })); + + await actionsClient.enqueueExecution({ + id: req.params.id, + spaceId: spaces ? spaces.spacesService.getSpaceId(req) : 'default', + apiKey: createAPIKeyResult + ? Buffer.from(`${createAPIKeyResult.id}:${createAPIKeyResult.api_key}`).toString( + 'base64' + ) + : null, + params: req.body.params, + source: { + type: 'HTTP_REQUEST' as any, + source: req, + }, + }); + return res.noContent(); + } catch (err) { + return res.badRequest({ body: err }); + } + } + ); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index fb32be12500c..53ea2b845af1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -177,12 +177,22 @@ export default function alertTests({ getService }: FtrProviderContext) { 'pre-7.10.0' ); + // Get scheduled task id + const getResponse = await supertestWithoutAuth + .get(`${getUrlPrefix(space.id)}/api/alerting/rule/${alertId}`) + .auth(user.username, user.password) + .expect(200); + // loading the archive likely caused the task to fail so ensure it's rescheduled to run in 2 seconds, // otherwise this test will stall for 5 minutes // no other attributes are touched, only runAt, so unless it would have ran when runAt expired, it // won't run now await supertest - .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reschedule_task`) + .put( + `${getUrlPrefix(space.id)}/api/alerts_fixture/${ + getResponse.body.scheduled_task_id + }/reschedule_task` + ) .set('kbn-xsrf', 'foo') .send({ runAt: getRunAt(2000), diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts new file mode 100644 index 000000000000..b6e47df31527 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/enqueue.ts @@ -0,0 +1,142 @@ +/* + * 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 expect from '@kbn/expect'; +import { Spaces } from '../../scenarios'; +import { + ESTestIndexTool, + ES_TEST_INDEX_NAME, + getUrlPrefix, + ObjectRemover, +} from '../../../common/lib'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + const retry = getService('retry'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + + describe('enqueue', () => { + const objectRemover = new ObjectRemover(supertest); + + before(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + }); + after(async () => { + await esTestIndexTool.destroy(); + await objectRemover.removeAll(); + }); + + it('should handle enqueue request appropriately', async () => { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: 'test.index-record', + config: { + unencrypted: `This value shouldn't get encrypted`, + }, + secrets: { + encrypted: 'This value should be encrypted', + }, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const reference = `actions-enqueue-1:${Spaces.space1.id}:${createdAction.id}`; + const response = await supertest + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${createdAction.id}/enqueue_action` + ) + .set('kbn-xsrf', 'foo') + .send({ + params: { + reference, + index: ES_TEST_INDEX_NAME, + message: 'Testing 123', + }, + }); + + expect(response.status).to.eql(204); + await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1); + }); + + it('should cleanup task after a failure', async () => { + const testStart = new Date(); + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'My action', + connector_type_id: 'test.failing', + config: {}, + secrets: {}, + }) + .expect(200); + objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions'); + + const reference = `actions-enqueue-2:${Spaces.space1.id}:${createdAction.id}`; + await supertest + .post( + `${getUrlPrefix(Spaces.space1.id)}/api/alerts_fixture/${createdAction.id}/enqueue_action` + ) + .set('kbn-xsrf', 'foo') + .send({ + params: { + reference, + index: ES_TEST_INDEX_NAME, + }, + }) + .expect(204); + + await esTestIndexTool.waitForDocs('action:test.failing', reference, 1); + + await supertest + .put( + `${getUrlPrefix( + Spaces.space1.id + )}/api/alerts_fixture/Actions-cleanup_failed_action_executions/reschedule_task` + ) + .set('kbn-xsrf', 'foo') + .send({ + runAt: new Date().toISOString(), + }) + .expect(200); + + await retry.try(async () => { + const searchResult = await es.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + term: { + 'task.taskType': 'actions:test.failing', + }, + }, + { + range: { + 'task.scheduledAt': { + gte: testStart, + }, + }, + }, + ], + }, + }, + }, + }); + expect(searchResult.hits.total.value).to.eql(0); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts index 43f442c13162..fc0b23290a86 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/index.ts @@ -21,6 +21,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./connector_types')); loadTestFile(require.resolve('./update')); loadTestFile(require.resolve('./execute')); + loadTestFile(require.resolve('./enqueue')); loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/webhook')); loadTestFile(require.resolve('./builtin_action_types/preconfigured_alert_history_connector'));