diff --git a/.buildkite/package-lock.json b/.buildkite/package-lock.json
index 04e3c73fdd2f5..d848470b3ff68 100644
--- a/.buildkite/package-lock.json
+++ b/.buildkite/package-lock.json
@@ -8,7 +8,7 @@
"name": "kibana-buildkite",
"version": "1.0.0",
"dependencies": {
- "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40"
+ "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4"
}
},
"node_modules/@nodelib/fs.scandir": {
@@ -355,8 +355,8 @@
},
"node_modules/kibana-buildkite-library": {
"version": "1.0.0",
- "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ef419d4f761dd256cb59bfab9b59f8b91029eb40",
- "integrity": "sha512-ou/Db/DAhyMeD0uQeLpJ/VfUCg0PGPssIYsr4gJZSTZoqxCDrMtE4nhtH8sXErHq0TaugdqergUhyhVHNBSJiA==",
+ "resolved": "git+https://git@github.com/elastic/kibana-buildkite-library.git#b9f6b423059cac7554a7402277f2ad3ecfe132a4",
+ "integrity": "sha512-HsSPeCrKwJKa+1urq/AzELmA1hsrwZjmOKzWzEYcQ63ZAnn8G3QWrGL0dNZQxIto3243EEs6Ne1pUVTMtEJr+Q==",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^18.10.0",
@@ -801,9 +801,9 @@
}
},
"kibana-buildkite-library": {
- "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#ef419d4f761dd256cb59bfab9b59f8b91029eb40",
- "integrity": "sha512-ou/Db/DAhyMeD0uQeLpJ/VfUCg0PGPssIYsr4gJZSTZoqxCDrMtE4nhtH8sXErHq0TaugdqergUhyhVHNBSJiA==",
- "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40",
+ "version": "git+https://git@github.com/elastic/kibana-buildkite-library.git#b9f6b423059cac7554a7402277f2ad3ecfe132a4",
+ "integrity": "sha512-HsSPeCrKwJKa+1urq/AzELmA1hsrwZjmOKzWzEYcQ63ZAnn8G3QWrGL0dNZQxIto3243EEs6Ne1pUVTMtEJr+Q==",
+ "from": "kibana-buildkite-library@git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4",
"requires": {
"@octokit/rest": "^18.10.0",
"axios": "^0.21.4",
diff --git a/.buildkite/package.json b/.buildkite/package.json
index daff8bd5db781..4e46ba6637027 100644
--- a/.buildkite/package.json
+++ b/.buildkite/package.json
@@ -3,6 +3,6 @@
"version": "1.0.0",
"private": true,
"dependencies": {
- "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#ef419d4f761dd256cb59bfab9b59f8b91029eb40"
+ "kibana-buildkite-library": "git+https://git@github.com/elastic/kibana-buildkite-library#b9f6b423059cac7554a7402277f2ad3ecfe132a4"
}
}
diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc
index aa5d9f53359b7..95003a08b7b09 100644
--- a/docs/settings/alert-action-settings.asciidoc
+++ b/docs/settings/alert-action-settings.asciidoc
@@ -187,37 +187,37 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`.
[[alert-settings]]
==== Alerting settings
-`xpack.alerting.maxEphemeralActionsPerAlert`::
+`xpack.alerting.maxEphemeralActionsPerAlert` {ess-icon}::
Sets the number of actions that will run ephemerally. To use this, enable
ephemeral tasks in task manager first with
<>
-`xpack.alerting.cancelAlertsOnRuleTimeout`::
+`xpack.alerting.cancelAlertsOnRuleTimeout` {ess-icon}::
Specifies whether to skip writing alerts and scheduling actions if rule
processing was cancelled due to a timeout. Default: `true`. This setting can be
overridden by individual rule types.
-`xpack.alerting.rules.minimumScheduleInterval.value`::
+`xpack.alerting.rules.minimumScheduleInterval.value` {ess-icon}::
Specifies the minimum schedule interval for rules. This minimum is applied to all rules created or updated after you set this value. The time is formatted as:
+
`[s,m,h,d]`
+
For example, `20m`, `24h`, `7d`. This duration cannot exceed `1d`. Default: `1m`.
-`xpack.alerting.rules.minimumScheduleInterval.enforce`::
+`xpack.alerting.rules.minimumScheduleInterval.enforce` {ess-icon}::
Specifies the behavior when a new or changed rule has a schedule interval less than the value defined in `xpack.alerting.rules.minimumScheduleInterval.value`. If `false`, rules with schedules less than the interval will be created but warnings will be logged. If `true`, rules with schedules less than the interval cannot be created. Default: `false`.
-`xpack.alerting.rules.run.actions.max`::
+`xpack.alerting.rules.run.actions.max` {ess-icon}::
Specifies the maximum number of actions that a rule can generate each time detection checks run.
-`xpack.alerting.rules.run.timeout`::
+`xpack.alerting.rules.run.timeout` {ess-icon}::
Specifies the default timeout for tasks associated with all types of rules. The time is formatted as:
+
`[ms,s,m,h,d,w,M,Y]`
+
For example, `20m`, `24h`, `7d`, `1w`. Default: `5m`.
-`xpack.alerting.rules.run.ruleTypeOverrides`::
+`xpack.alerting.rules.run.ruleTypeOverrides` {ess-icon}::
Overrides the configs under `xpack.alerting.rules.run` for the rule type with the given ID. List the rule identifier and its settings in an array of objects.
+
For example:
@@ -230,7 +230,7 @@ xpack.alerting.rules.run:
timeout: '15m'
--
-`xpack.alerting.rules.run.actions.connectorTypeOverrides`::
+`xpack.alerting.rules.run.actions.connectorTypeOverrides` {ess-icon}::
Overrides the configs under `xpack.alerting.rules.run.actions` for the connector type with the given ID. List the connector type identifier and its settings in an array of objects.
+
For example:
diff --git a/package.json b/package.json
index 2f0a37ff47735..b0b21934009c5 100644
--- a/package.json
+++ b/package.json
@@ -279,9 +279,7 @@
"https-proxy-agent": "^5.0.0",
"i18n-iso-countries": "^4.3.1",
"icalendar": "0.7.1",
- "idx": "^2.5.6",
"immer": "^9.0.6",
- "inline-style": "^2.0.0",
"inquirer": "^7.3.3",
"intl": "^1.2.5",
"intl-format-cache": "^2.1.0",
@@ -415,7 +413,6 @@
"styled-components": "^5.1.0",
"suricata-sid-db": "^1.0.2",
"symbol-observable": "^1.2.0",
- "tabbable": "1.1.3",
"tar": "^6.1.11",
"tinycolor2": "1.4.1",
"tinygradient": "0.4.3",
@@ -438,7 +435,6 @@
"vega-tooltip": "^0.28.0",
"venn.js": "0.2.20",
"vinyl": "^2.2.0",
- "vt-pbf": "^3.1.1",
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.4.22",
"yauzl": "^2.10.0"
@@ -715,7 +711,6 @@
"@types/opn": "^5.1.0",
"@types/ora": "^1.3.5",
"@types/papaparse": "^5.0.3",
- "@types/parse-link-header": "^1.0.0",
"@types/pbf": "3.0.2",
"@types/pdfmake": "^0.1.19",
"@types/pegjs": "^0.10.1",
@@ -757,7 +752,6 @@
"@types/supertest": "^2.0.5",
"@types/tapable": "^1.0.6",
"@types/tar": "^4.0.5",
- "@types/tar-fs": "^1.16.1",
"@types/tempy": "^0.2.0",
"@types/testing-library__jest-dom": "^5.14.3",
"@types/tinycolor2": "^1.4.1",
@@ -854,7 +848,6 @@
"file-loader": "^4.2.0",
"form-data": "^4.0.0",
"geckodriver": "^3.0.1",
- "glob-watcher": "5.0.3",
"gulp": "4.0.2",
"gulp-babel": "^8.0.0",
"gulp-brotli": "^3.0.0",
@@ -894,13 +887,11 @@
"marge": "^1.0.1",
"micromatch": "3.1.10",
"minimist": "^1.2.6",
- "mkdirp": "0.5.1",
"mocha": "^9.1.0",
"mocha-junit-reporter": "^2.0.2",
"mochawesome": "^7.0.1",
"mochawesome-merge": "^4.2.1",
"mock-fs": "^5.1.2",
- "mock-http-server": "1.3.0",
"ms-chromium-edge-driver": "^0.5.1",
"multimatch": "^4.0.0",
"mutation-observer": "^1.0.3",
@@ -910,10 +901,8 @@
"nyc": "^15.1.0",
"oboe": "^2.1.4",
"openapi-types": "^10.0.0",
- "parse-link-header": "^1.0.1",
"pbf": "3.2.1",
"pirates": "^4.0.1",
- "pixelmatch": "^5.1.0",
"playwright": "^1.17.1",
"postcss": "^7.0.32",
"postcss-loader": "^3.0.0",
@@ -927,9 +916,7 @@
"resolve": "^1.22.0",
"rxjs-marbles": "^5.0.6",
"sass-loader": "^10.2.0",
- "sass-resources-loader": "^2.0.1",
"selenium-webdriver": "^4.1.1",
- "serve-static": "1.14.1",
"shelljs": "^0.8.4",
"simple-git": "1.116.0",
"sinon": "^7.4.2",
@@ -945,7 +932,6 @@
"supertest": "^3.1.0",
"supports-color": "^7.0.0",
"tape": "^5.0.1",
- "tar-fs": "^2.1.0",
"tempy": "^0.3.0",
"terser": "^5.7.1",
"terser-webpack-plugin": "^4.2.3",
diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml
index 9f73dcd620d30..9baed7a92a53e 100644
--- a/packages/kbn-optimizer/limits.yml
+++ b/packages/kbn-optimizer/limits.yml
@@ -57,7 +57,7 @@ pageLoadAssetSize:
telemetry: 51957
telemetryManagementSection: 38586
transform: 41007
- triggersActionsUi: 103400
+ triggersActionsUi: 104400
upgradeAssistant: 81241
urlForwarding: 32579
usageCollection: 39762
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index 184c16f96167f..f2d5a60cd325e 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -19305,7 +19305,7 @@ cmdShim.ifExists = cmdShimIfExists
var fs = __webpack_require__("../../node_modules/graceful-fs/graceful-fs.js")
-var mkdir = __webpack_require__("../../node_modules/cmd-shim/node_modules/mkdirp/index.js")
+var mkdir = __webpack_require__("../../node_modules/mkdirp/index.js")
, path = __webpack_require__("path")
, toBatchSyntax = __webpack_require__("../../node_modules/cmd-shim/lib/to-batch-syntax.js")
, shebangExpr = /^#\!\s*(?:\/usr\/bin\/env)?\s*([^ \t]+=[^ \t]+\s+)*\s*([^ \t]+)(.*)$/
@@ -19598,112 +19598,6 @@ function replaceDollarWithPercentPair(value) {
-/***/ }),
-
-/***/ "../../node_modules/cmd-shim/node_modules/mkdirp/index.js":
-/***/ (function(module, exports, __webpack_require__) {
-
-var path = __webpack_require__("path");
-var fs = __webpack_require__("fs");
-var _0777 = parseInt('0777', 8);
-
-module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP;
-
-function mkdirP (p, opts, f, made) {
- if (typeof opts === 'function') {
- f = opts;
- opts = {};
- }
- else if (!opts || typeof opts !== 'object') {
- opts = { mode: opts };
- }
-
- var mode = opts.mode;
- var xfs = opts.fs || fs;
-
- if (mode === undefined) {
- mode = _0777 & (~process.umask());
- }
- if (!made) made = null;
-
- var cb = f || function () {};
- p = path.resolve(p);
-
- xfs.mkdir(p, mode, function (er) {
- if (!er) {
- made = made || p;
- return cb(null, made);
- }
- switch (er.code) {
- case 'ENOENT':
- if (path.dirname(p) === p) return cb(er);
- mkdirP(path.dirname(p), opts, function (er, made) {
- if (er) cb(er, made);
- else mkdirP(p, opts, cb, made);
- });
- break;
-
- // In the case of any other error, just see if there's a dir
- // there already. If so, then hooray! If not, then something
- // is borked.
- default:
- xfs.stat(p, function (er2, stat) {
- // if the stat fails, then that's super weird.
- // let the original error be the failure reason.
- if (er2 || !stat.isDirectory()) cb(er, made)
- else cb(null, made);
- });
- break;
- }
- });
-}
-
-mkdirP.sync = function sync (p, opts, made) {
- if (!opts || typeof opts !== 'object') {
- opts = { mode: opts };
- }
-
- var mode = opts.mode;
- var xfs = opts.fs || fs;
-
- if (mode === undefined) {
- mode = _0777 & (~process.umask());
- }
- if (!made) made = null;
-
- p = path.resolve(p);
-
- try {
- xfs.mkdirSync(p, mode);
- made = made || p;
- }
- catch (err0) {
- switch (err0.code) {
- case 'ENOENT' :
- made = sync(path.dirname(p), opts, made);
- sync(p, opts, made);
- break;
-
- // In the case of any other error, just see if there's a dir
- // there already. If so, then hooray! If not, then something
- // is borked.
- default:
- var stat;
- try {
- stat = xfs.statSync(p);
- }
- catch (err1) {
- throw err0;
- }
- if (!stat.isDirectory()) throw err0;
- break;
- }
- }
-
- return made;
-};
-
-
/***/ }),
/***/ "../../node_modules/color-convert/conversions.js":
@@ -36304,6 +36198,112 @@ function isConstructorOrProto (obj, key) {
}
+/***/ }),
+
+/***/ "../../node_modules/mkdirp/index.js":
+/***/ (function(module, exports, __webpack_require__) {
+
+var path = __webpack_require__("path");
+var fs = __webpack_require__("fs");
+var _0777 = parseInt('0777', 8);
+
+module.exports = mkdirP.mkdirp = mkdirP.mkdirP = mkdirP;
+
+function mkdirP (p, opts, f, made) {
+ if (typeof opts === 'function') {
+ f = opts;
+ opts = {};
+ }
+ else if (!opts || typeof opts !== 'object') {
+ opts = { mode: opts };
+ }
+
+ var mode = opts.mode;
+ var xfs = opts.fs || fs;
+
+ if (mode === undefined) {
+ mode = _0777 & (~process.umask());
+ }
+ if (!made) made = null;
+
+ var cb = f || function () {};
+ p = path.resolve(p);
+
+ xfs.mkdir(p, mode, function (er) {
+ if (!er) {
+ made = made || p;
+ return cb(null, made);
+ }
+ switch (er.code) {
+ case 'ENOENT':
+ if (path.dirname(p) === p) return cb(er);
+ mkdirP(path.dirname(p), opts, function (er, made) {
+ if (er) cb(er, made);
+ else mkdirP(p, opts, cb, made);
+ });
+ break;
+
+ // In the case of any other error, just see if there's a dir
+ // there already. If so, then hooray! If not, then something
+ // is borked.
+ default:
+ xfs.stat(p, function (er2, stat) {
+ // if the stat fails, then that's super weird.
+ // let the original error be the failure reason.
+ if (er2 || !stat.isDirectory()) cb(er, made)
+ else cb(null, made);
+ });
+ break;
+ }
+ });
+}
+
+mkdirP.sync = function sync (p, opts, made) {
+ if (!opts || typeof opts !== 'object') {
+ opts = { mode: opts };
+ }
+
+ var mode = opts.mode;
+ var xfs = opts.fs || fs;
+
+ if (mode === undefined) {
+ mode = _0777 & (~process.umask());
+ }
+ if (!made) made = null;
+
+ p = path.resolve(p);
+
+ try {
+ xfs.mkdirSync(p, mode);
+ made = made || p;
+ }
+ catch (err0) {
+ switch (err0.code) {
+ case 'ENOENT' :
+ made = sync(path.dirname(p), opts, made);
+ sync(p, opts, made);
+ break;
+
+ // In the case of any other error, just see if there's a dir
+ // there already. If so, then hooray! If not, then something
+ // is borked.
+ default:
+ var stat;
+ try {
+ stat = xfs.statSync(p);
+ }
+ catch (err1) {
+ throw err0;
+ }
+ if (!stat.isDirectory()) throw err0;
+ break;
+ }
+ }
+
+ return made;
+};
+
+
/***/ }),
/***/ "../../node_modules/multimatch/index.js":
diff --git a/packages/kbn-test-jest-helpers/BUILD.bazel b/packages/kbn-test-jest-helpers/BUILD.bazel
index dc8b83495494c..85192829003e4 100644
--- a/packages/kbn-test-jest-helpers/BUILD.bazel
+++ b/packages/kbn-test-jest-helpers/BUILD.bazel
@@ -60,7 +60,6 @@ RUNTIME_DEPS = [
"@npm//joi",
"@npm//mustache",
"@npm//normalize-path",
- "@npm//parse-link-header",
"@npm//prettier",
"@npm//react",
"@npm//react-dom",
@@ -106,7 +105,6 @@ TYPES_DEPS = [
"@npm//@types/mustache",
"@npm//@types/normalize-path",
"@npm//@types/node",
- "@npm//@types/parse-link-header",
"@npm//@types/prettier",
"@npm//@types/react",
"@npm//@types/react-dom",
diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel
index f7599e6d81649..15487aa781b8d 100644
--- a/packages/kbn-test/BUILD.bazel
+++ b/packages/kbn-test/BUILD.bazel
@@ -67,7 +67,6 @@ RUNTIME_DEPS = [
"@npm//js-yaml",
"@npm//mustache",
"@npm//normalize-path",
- "@npm//parse-link-header",
"@npm//prettier",
"@npm//react-dom",
"@npm//react-redux",
@@ -115,7 +114,6 @@ TYPES_DEPS = [
"@npm//@types/mustache",
"@npm//@types/normalize-path",
"@npm//@types/node",
- "@npm//@types/parse-link-header",
"@npm//@types/prettier",
"@npm//@types/react-dom",
"@npm//@types/react-redux",
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
index 955d69509cf01..7b715bb56a74c 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
@@ -156,9 +156,9 @@ function DiscoverDocumentsComponent({
>
)}
{!isLegacy && (
-
+ >
)}
);
diff --git a/src/plugins/discover/public/components/discover_grid/constants.ts b/src/plugins/discover/public/components/discover_grid/constants.ts
index d026607aef373..f2f5a8e8bebc7 100644
--- a/src/plugins/discover/public/components/discover_grid/constants.ts
+++ b/src/plugins/discover/public/components/discover_grid/constants.ts
@@ -19,7 +19,7 @@ export const GRID_STYLE = {
export const pageSizeArr = [25, 50, 100, 250];
export const defaultPageSize = 100;
-export const defaultTimeColumnWidth = 190;
+export const defaultTimeColumnWidth = 210;
export const toolbarVisibility = {
showColumnSelector: {
allowHide: false,
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/components/discover_grid/discover_grid.scss
index 0204433a5ba1c..113bb60924850 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid.scss
@@ -30,6 +30,15 @@
}
}
+.dscDiscoverGrid__cellValue {
+ font-family: $euiCodeFontFamily;
+}
+
+.dscDiscoverGrid__cellPopoverValue {
+ font-family: $euiCodeFontFamily;
+ font-size: $euiFontSizeS;
+}
+
.dscDiscoverGrid__footer {
background-color: $euiColorLightShade;
padding: $euiSize / 2 $euiSize;
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx
index a9116e616946f..c98db31a97f7f 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.test.tsx
@@ -207,7 +207,7 @@ describe('Discover grid columns', function () {
/>
,
"id": "timestamp",
- "initialWidth": 190,
+ "initialWidth": 210,
"isSortable": true,
"schema": "datetime",
},
diff --git a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx
index be4c69f1ced25..53e5c23cb47d5 100644
--- a/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx
+++ b/src/plugins/discover/public/components/discover_grid/get_render_cell_value.test.tsx
@@ -92,7 +92,9 @@ describe('Discover grid cell rendering', function () {
setCellProps={jest.fn()}
/>
);
- expect(component.html()).toMatchInlineSnapshot(`"100"`);
+ expect(component.html()).toMatchInlineSnapshot(
+ `"100"`
+ );
});
it('renders bytes column correctly using _source when details is true', () => {
@@ -115,7 +117,9 @@ describe('Discover grid cell rendering', function () {
setCellProps={jest.fn()}
/>
);
- expect(component.html()).toMatchInlineSnapshot(`"100"`);
+ expect(component.html()).toMatchInlineSnapshot(
+ `"100"`
+ );
});
it('renders bytes column correctly using fields when details is true', () => {
@@ -138,7 +142,9 @@ describe('Discover grid cell rendering', function () {
setCellProps={jest.fn()}
/>
);
- expect(component.html()).toMatchInlineSnapshot(`"100"`);
+ expect(component.html()).toMatchInlineSnapshot(
+ `"100"`
+ );
});
it('renders _source column correctly', () => {
@@ -163,7 +169,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
@@ -280,7 +286,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
@@ -359,7 +365,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
@@ -485,7 +491,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
@@ -527,7 +533,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
@@ -603,6 +609,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
);
- expect(component.html()).toMatchInlineSnapshot(`"-"`);
+ expect(component.html()).toMatchInlineSnapshot(
+ `"-"`
+ );
});
it('renders correctly when invalid column is given', () => {
@@ -657,7 +666,9 @@ describe('Discover grid cell rendering', function () {
setCellProps={jest.fn()}
/>
);
- expect(component.html()).toMatchInlineSnapshot(`"-"`);
+ expect(component.html()).toMatchInlineSnapshot(
+ `"-"`
+ );
});
it('renders unmapped fields correctly', () => {
@@ -695,6 +706,7 @@ describe('Discover grid cell rendering', function () {
);
expect(component).toMatchInlineSnapshot(`
-;
+ return -;
}
/**
@@ -102,7 +105,11 @@ export const getRenderCellValueFn =
: formatHit(row, dataView, fieldsToShow, maxEntries, fieldFormats);
return (
-
+
{pairs.map(([key, value]) => (
{key}
@@ -118,6 +125,7 @@ export const getRenderCellValueFn =
return (
{
update: jest.fn(),
getAll: jest.fn(),
getBulk: jest.fn(),
+ getOAuthAccessToken: jest.fn(),
execute: jest.fn(),
enqueueExecution: jest.fn(),
ephemeralEnqueuedExecution: jest.fn(),
diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts
index afee13b8c9bca..787b4e450a9e0 100644
--- a/x-pack/plugins/actions/server/actions_client.test.ts
+++ b/x-pack/plugins/actions/server/actions_client.test.ts
@@ -18,11 +18,14 @@ import { actionsConfigMock } from './actions_config.mock';
import { getActionsConfigurationUtilities } from './actions_config';
import { licenseStateMock } from './lib/license_state.mock';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
-import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks';
+import {
+ httpServerMock,
+ loggingSystemMock,
+ elasticsearchServiceMock,
+ savedObjectsClientMock,
+} from '@kbn/core/server/mocks';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
-
-import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { actionExecutorMock } from './lib/action_executor.mock';
import uuid from 'uuid';
import { ActionsAuthorization } from './authorization/actions_authorization';
@@ -37,6 +40,9 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s
import { Logger } from '@kbn/core/server';
import { connectorTokenClientMock } from './builtin_action_types/lib/connector_token_client.mock';
import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock';
+import { getOAuthJwtAccessToken } from './builtin_action_types/lib/get_oauth_jwt_access_token';
+import { getOAuthClientCredentialsAccessToken } from './builtin_action_types/lib/get_oauth_client_credentials_access_token';
+import { OAuthParams } from './routes/get_oauth_access_token';
jest.mock('@kbn/core/server/saved_objects/service/lib/utils', () => ({
SavedObjectsUtils: {
@@ -60,6 +66,13 @@ jest.mock('./authorization/get_authorization_mode_by_source', () => {
};
});
+jest.mock('./builtin_action_types/lib/get_oauth_jwt_access_token', () => ({
+ getOAuthJwtAccessToken: jest.fn(),
+}));
+jest.mock('./builtin_action_types/lib/get_oauth_client_credentials_access_token', () => ({
+ getOAuthClientCredentialsAccessToken: jest.fn(),
+}));
+
const defaultKibanaIndex = '.kibana';
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const scopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
@@ -73,6 +86,7 @@ const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');
const logger = loggingSystemMock.create().get() as jest.Mocked;
const mockTaskManager = taskManagerMock.createSetup();
+const configurationUtilities = actionsConfigMock.create();
let actionsClient: ActionsClient;
let mockedLicenseState: jest.Mocked;
@@ -115,6 +129,10 @@ beforeEach(() => {
usageCounter: mockUsageCounter,
connectorTokenClient,
});
+ (getOAuthJwtAccessToken as jest.Mock).mockResolvedValue(`Bearer jwttokentokentoken`);
+ (getOAuthClientCredentialsAccessToken as jest.Mock).mockResolvedValue(
+ `Bearer clienttokentokentoken`
+ );
});
describe('create()', () => {
@@ -1274,6 +1292,292 @@ describe('getBulk()', () => {
});
});
+describe('getOAuthAccessToken()', () => {
+ function getOAuthAccessToken(
+ requestBody: OAuthParams
+ ): ReturnType {
+ actionsClient = new ActionsClient({
+ actionTypeRegistry,
+ unsecuredSavedObjectsClient,
+ scopedClusterClient,
+ defaultKibanaIndex,
+ actionExecutor,
+ executionEnqueuer,
+ ephemeralExecutionEnqueuer,
+ request,
+ authorization: authorization as unknown as ActionsAuthorization,
+ preconfiguredActions: [
+ {
+ id: 'testPreconfigured',
+ actionTypeId: '.slack',
+ secrets: {},
+ isPreconfigured: true,
+ isDeprecated: false,
+ name: 'test',
+ config: {
+ foo: 'bar',
+ },
+ },
+ ],
+ connectorTokenClient: connectorTokenClientMock.create(),
+ });
+ return actionsClient.getOAuthAccessToken(requestBody, logger, configurationUtilities);
+ }
+
+ describe('authorization', () => {
+ test('ensures user is authorised to get the type of action', async () => {
+ await getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ });
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ });
+
+ test('throws when user is not authorised to create the type of action', async () => {
+ authorization.ensureAuthorized.mockRejectedValue(new Error(`Unauthorized to update actions`));
+
+ await expect(
+ getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Unauthorized to update actions]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ });
+ });
+
+ test('throws when tokenUrl is not using http or https', async () => {
+ await expect(
+ getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: 'ftp://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Token URL must use http or https]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ });
+
+ test('throws when tokenUrl does not contain hostname', async () => {
+ await expect(
+ getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: '/path/to/myfile',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Token URL must contain hostname]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ });
+
+ test('throws when tokenUrl is not in allowed hosts', async () => {
+ configurationUtilities.ensureUriAllowed.mockImplementationOnce(() => {
+ throw new Error('URI not allowed');
+ });
+
+ await expect(
+ getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: URI not allowed]`);
+
+ expect(authorization.ensureAuthorized).toHaveBeenCalledWith('update');
+ expect(configurationUtilities.ensureUriAllowed).toHaveBeenCalledWith(
+ `https://testurl.service-now.com/oauth_token.do`
+ );
+ });
+
+ test('calls getOAuthJwtAccessToken when type="jwt"', async () => {
+ const result = await getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ });
+ expect(result).toEqual({
+ accessToken: 'Bearer jwttokentokentoken',
+ });
+ expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ });
+ expect(getOAuthClientCredentialsAccessToken).not.toHaveBeenCalled();
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Successfully retrieved access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"}`
+ );
+ });
+
+ test('calls getOAuthClientCredentialsAccessToken when type="client"', async () => {
+ const result = await getOAuthAccessToken({
+ type: 'client',
+ options: {
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ scope: 'https://graph.microsoft.com/.default',
+ config: {
+ clientId: 'abc',
+ tenantId: 'def',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ },
+ },
+ });
+ expect(result).toEqual({
+ accessToken: 'Bearer clienttokentokentoken',
+ });
+ expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalledWith({
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'abc',
+ tenantId: 'def',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ },
+ },
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ oAuthScope: 'https://graph.microsoft.com/.default',
+ });
+ expect(getOAuthJwtAccessToken).not.toHaveBeenCalled();
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Successfully retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"}`
+ );
+ });
+
+ test('throws when getOAuthJwtAccessToken throws error', async () => {
+ (getOAuthJwtAccessToken as jest.Mock).mockRejectedValue(new Error(`Something went wrong!`));
+
+ await expect(
+ getOAuthAccessToken({
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`);
+
+ expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalled();
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Failed to retrieve access token using JWT OAuth with tokenUrl https://testurl.service-now.com/oauth_token.do and config {\"clientId\":\"abc\",\"jwtKeyId\":\"def\",\"userIdentifierValue\":\"userA\"} - Something went wrong!`
+ );
+ });
+
+ test('throws when getOAuthClientCredentialsAccessToken throws error', async () => {
+ (getOAuthClientCredentialsAccessToken as jest.Mock).mockRejectedValue(
+ new Error(`Something went wrong!`)
+ );
+
+ await expect(
+ getOAuthAccessToken({
+ type: 'client',
+ options: {
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ scope: 'https://graph.microsoft.com/.default',
+ config: {
+ clientId: 'abc',
+ tenantId: 'def',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ },
+ },
+ })
+ ).rejects.toMatchInlineSnapshot(`[Error: Failed to retrieve access token]`);
+
+ expect(getOAuthClientCredentialsAccessToken as jest.Mock).toHaveBeenCalled();
+ expect(logger.debug).toHaveBeenCalledWith(
+ `Failed to retrieved access token using Client Credentials OAuth with tokenUrl https://login.microsoftonline.com/98765/oauth2/v2.0/token, scope https://graph.microsoft.com/.default and config {\"clientId\":\"abc\",\"tenantId\":\"def\"} - Something went wrong!`
+ );
+ });
+});
+
describe('delete()', () => {
describe('authorization', () => {
test('ensures user is authorised to delete actions', async () => {
diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts
index dacf6de36bd37..89156bb56b51a 100644
--- a/x-pack/plugins/actions/server/actions_client.ts
+++ b/x-pack/plugins/actions/server/actions_client.ts
@@ -6,6 +6,7 @@
*/
import Boom from '@hapi/boom';
+import url from 'url';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
@@ -18,6 +19,7 @@ import {
SavedObject,
KibanaRequest,
SavedObjectsUtils,
+ Logger,
} from '@kbn/core/server';
import { AuditLogger } from '@kbn/security-plugin/server';
import { RunNowResult } from '@kbn/task-manager-plugin/server';
@@ -46,6 +48,22 @@ import {
import { connectorAuditEvent, ConnectorAuditAction } from './lib/audit_events';
import { trackLegacyRBACExemption } from './lib/track_legacy_rbac_exemption';
import { isConnectorDeprecated } from './lib/is_conector_deprecated';
+import { ActionsConfigurationUtilities } from './actions_config';
+import {
+ OAuthClientCredentialsParams,
+ OAuthJwtParams,
+ OAuthParams,
+} from './routes/get_oauth_access_token';
+import {
+ getOAuthJwtAccessToken,
+ GetOAuthJwtConfig,
+ GetOAuthJwtSecrets,
+} from './builtin_action_types/lib/get_oauth_jwt_access_token';
+import {
+ getOAuthClientCredentialsAccessToken,
+ GetOAuthClientCredentialsConfig,
+ GetOAuthClientCredentialsSecrets,
+} from './builtin_action_types/lib/get_oauth_client_credentials_access_token';
// We are assuming there won't be many actions. This is why we will load
// all the actions in advance and assume the total count to not go over 10000.
@@ -448,6 +466,98 @@ export class ActionsClient {
return actionResults;
}
+ public async getOAuthAccessToken(
+ { type, options }: OAuthParams,
+ logger: Logger,
+ configurationUtilities: ActionsConfigurationUtilities
+ ) {
+ // Verify that user has edit access
+ await this.authorization.ensureAuthorized('update');
+
+ // Verify that token url is allowed by allowed hosts config
+ try {
+ configurationUtilities.ensureUriAllowed(options.tokenUrl);
+ } catch (err) {
+ throw Boom.badRequest(err.message);
+ }
+
+ // Verify that token url contains a hostname and uses https
+ const parsedUrl = url.parse(
+ options.tokenUrl,
+ false /* parseQueryString */,
+ true /* slashesDenoteHost */
+ );
+
+ if (!parsedUrl.hostname) {
+ throw Boom.badRequest(`Token URL must contain hostname`);
+ }
+
+ if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
+ throw Boom.badRequest(`Token URL must use http or https`);
+ }
+
+ let accessToken: string | null = null;
+ if (type === 'jwt') {
+ const tokenOpts = options as OAuthJwtParams;
+
+ try {
+ accessToken = await getOAuthJwtAccessToken({
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: tokenOpts.config as GetOAuthJwtConfig,
+ secrets: tokenOpts.secrets as GetOAuthJwtSecrets,
+ },
+ tokenUrl: tokenOpts.tokenUrl,
+ });
+
+ logger.debug(
+ `Successfully retrieved access token using JWT OAuth with tokenUrl ${
+ tokenOpts.tokenUrl
+ } and config ${JSON.stringify(tokenOpts.config)}`
+ );
+ } catch (err) {
+ logger.debug(
+ `Failed to retrieve access token using JWT OAuth with tokenUrl ${
+ tokenOpts.tokenUrl
+ } and config ${JSON.stringify(tokenOpts.config)} - ${err.message}`
+ );
+ throw Boom.badRequest(`Failed to retrieve access token`);
+ }
+ } else if (type === 'client') {
+ const tokenOpts = options as OAuthClientCredentialsParams;
+ try {
+ accessToken = await getOAuthClientCredentialsAccessToken({
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: tokenOpts.config as GetOAuthClientCredentialsConfig,
+ secrets: tokenOpts.secrets as GetOAuthClientCredentialsSecrets,
+ },
+ tokenUrl: tokenOpts.tokenUrl,
+ oAuthScope: tokenOpts.scope,
+ });
+
+ logger.debug(
+ `Successfully retrieved access token using Client Credentials OAuth with tokenUrl ${
+ tokenOpts.tokenUrl
+ }, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)}`
+ );
+ } catch (err) {
+ logger.debug(
+ `Failed to retrieved access token using Client Credentials OAuth with tokenUrl ${
+ tokenOpts.tokenUrl
+ }, scope ${tokenOpts.scope} and config ${JSON.stringify(tokenOpts.config)} - ${
+ err.message
+ }`
+ );
+ throw Boom.badRequest(`Failed to retrieve access token`);
+ }
+ }
+
+ return { accessToken };
+ }
+
/**
* Delete action
*/
diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts
index 470e6ce8cdc8e..a6b68d907cb44 100644
--- a/x-pack/plugins/actions/server/actions_config.test.ts
+++ b/x-pack/plugins/actions/server/actions_config.test.ts
@@ -129,6 +129,17 @@ describe('isUriAllowed', () => {
).toEqual(true);
});
+ test('returns true for network path references', () => {
+ const config: ActionsConfig = {
+ ...defaultActionsConfig,
+ allowedHosts: ['my-domain.com'],
+ enabledActionTypes: [],
+ };
+ expect(getActionsConfigurationUtilities(config).isUriAllowed('//my-domain.com/foo')).toEqual(
+ true
+ );
+ });
+
test('throws when the hostname in the requested uri is not in the allowedHosts', () => {
const config: ActionsConfig = defaultActionsConfig;
expect(
diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts
index 35e08bb5cfe66..49f1d1fd5445e 100644
--- a/x-pack/plugins/actions/server/actions_config.ts
+++ b/x-pack/plugins/actions/server/actions_config.ts
@@ -76,7 +76,7 @@ function isAllowed({ allowedHosts }: ActionsConfig, hostname: string | null): bo
function isHostnameAllowedInUri(config: ActionsConfig, uri: string): boolean {
return pipe(
- tryCatch(() => url.parse(uri)),
+ tryCatch(() => url.parse(uri, false /* parseQueryString */, true /* slashesDenoteHost */)),
map((parsedUrl) => parsedUrl.hostname),
mapNullable((hostname) => isAllowed(config, hostname)),
getOrElse(() => false)
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts
index b33a2d17ed9d8..9dde4790c152d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/create_jwt_assertion.ts
@@ -20,8 +20,7 @@ export function createJWTAssertion(
logger: Logger,
privateKey: string,
privateKeyPassword: string | null,
- reservedClaims: JWTClaims,
- customClaims?: Record
+ reservedClaims: JWTClaims
): string {
const { subject, audience, issuer, expireInMilliseconds, keyId } = reservedClaims;
const iat = Math.floor(Date.now() / 1000);
@@ -34,7 +33,6 @@ export function createJWTAssertion(
iss: issuer, // issuer claim identifies the principal that issued the JWT
iat, // issued at claim identifies the time at which the JWT was issued
exp: iat + (expireInMilliseconds ?? 3600), // expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing
- ...(customClaims ?? {}),
};
try {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts
new file mode 100644
index 0000000000000..2efa79cf09c48
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.test.ts
@@ -0,0 +1,306 @@
+/*
+ * 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 '@kbn/core/server';
+import { asyncForEach } from '@kbn/std';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
+import { actionsConfigMock } from '../../actions_config.mock';
+import { connectorTokenClientMock } from './connector_token_client.mock';
+import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token';
+import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token';
+
+jest.mock('./request_oauth_client_credentials_token', () => ({
+ requestOAuthClientCredentialsToken: jest.fn(),
+}));
+
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+const configurationUtilities = actionsConfigMock.create();
+const connectorTokenClient = connectorTokenClientMock.create();
+
+describe('getOAuthClientCredentialsAccessToken', () => {
+ const getOAuthClientCredentialsAccessTokenOpts = {
+ connectorId: '123',
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ tenantId: 'tenantId',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ },
+ },
+ oAuthScope: 'https://graph.microsoft.com/.default',
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ connectorTokenClient,
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ jest.clearAllMocks();
+ });
+
+ test('uses stored access token if it exists', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: {
+ id: '1',
+ connectorId: '123',
+ tokenType: 'access_token',
+ token: 'testtokenvalue',
+ createdAt: new Date().toISOString(),
+ expiresAt: new Date(Date.now() + 10000000000).toISOString(),
+ },
+ });
+ const accessToken = await getOAuthClientCredentialsAccessToken(
+ getOAuthClientCredentialsAccessTokenOpts
+ );
+
+ expect(accessToken).toEqual('testtokenvalue');
+ expect(requestOAuthClientCredentialsToken as jest.Mock).not.toHaveBeenCalled();
+ });
+
+ test('creates new assertion if stored access token does not exist', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthClientCredentialsAccessToken(
+ getOAuthClientCredentialsAccessTokenOpts
+ );
+
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ logger,
+ {
+ scope: 'https://graph.microsoft.com/.default',
+ clientId: 'clientId',
+ clientSecret: 'clientSecret',
+ },
+
+ configurationUtilities
+ );
+ expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
+ connectorId: '123',
+ token: null,
+ newToken: 'access_token brandnewaccesstoken',
+ expiresInSec: 1000,
+ deleteExisting: false,
+ });
+ });
+
+ test('creates new assertion if stored access token exists but is expired', async () => {
+ const createdAt = new Date().toISOString();
+ const expiresAt = new Date(Date.now() - 100).toISOString();
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: {
+ id: '1',
+ connectorId: '123',
+ tokenType: 'access_token',
+ token: 'testtokenvalue',
+ createdAt,
+ expiresAt,
+ },
+ });
+ (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthClientCredentialsAccessToken(
+ getOAuthClientCredentialsAccessTokenOpts
+ );
+
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ logger,
+ {
+ scope: 'https://graph.microsoft.com/.default',
+ clientId: 'clientId',
+ clientSecret: 'clientSecret',
+ },
+
+ configurationUtilities
+ );
+ expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
+ connectorId: '123',
+ token: {
+ id: '1',
+ connectorId: '123',
+ tokenType: 'access_token',
+ token: 'testtokenvalue',
+ createdAt,
+ expiresAt,
+ },
+ newToken: 'access_token brandnewaccesstoken',
+ expiresInSec: 1000,
+ deleteExisting: false,
+ });
+ });
+
+ test('returns null and logs warning if any required fields are missing', async () => {
+ await asyncForEach(['clientId', 'tenantId'], async (configField: string) => {
+ const accessToken = await getOAuthClientCredentialsAccessToken({
+ ...getOAuthClientCredentialsAccessTokenOpts,
+ credentials: {
+ config: {
+ ...getOAuthClientCredentialsAccessTokenOpts.credentials.config,
+ [configField]: null,
+ },
+ secrets: getOAuthClientCredentialsAccessTokenOpts.credentials.secrets,
+ },
+ });
+ expect(accessToken).toBeNull();
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Missing required fields for requesting OAuth Client Credentials access token`
+ );
+ });
+
+ await asyncForEach(['clientSecret'], async (secretsField: string) => {
+ const accessToken = await getOAuthClientCredentialsAccessToken({
+ ...getOAuthClientCredentialsAccessTokenOpts,
+ credentials: {
+ config: getOAuthClientCredentialsAccessTokenOpts.credentials.config,
+ secrets: {
+ ...getOAuthClientCredentialsAccessTokenOpts.credentials.secrets,
+ [secretsField]: null,
+ },
+ },
+ });
+ expect(accessToken).toBeNull();
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Missing required fields for requesting OAuth Client Credentials access token`
+ );
+ });
+ });
+
+ test('throws error if requestOAuthClientCredentialsToken throws error', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (requestOAuthClientCredentialsToken as jest.Mock).mockRejectedValueOnce(
+ new Error('requestOAuthClientCredentialsToken error!!')
+ );
+
+ await expect(
+ getOAuthClientCredentialsAccessToken(getOAuthClientCredentialsAccessTokenOpts)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"requestOAuthClientCredentialsToken error!!"`);
+ });
+
+ test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+ connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error'));
+
+ const accessToken = await getOAuthClientCredentialsAccessToken(
+ getOAuthClientCredentialsAccessTokenOpts
+ );
+
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Not able to update connector token for connectorId: 123 due to error: updateOrReplace error`
+ );
+ });
+
+ test('gets access token if connectorId is not provided', async () => {
+ (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthClientCredentialsAccessToken({
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ tenantId: 'tenantId',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ },
+ },
+ oAuthScope: 'https://graph.microsoft.com/.default',
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ connectorTokenClient,
+ });
+
+ expect(connectorTokenClient.get).not.toHaveBeenCalled();
+ expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled();
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ logger,
+ {
+ scope: 'https://graph.microsoft.com/.default',
+ clientId: 'clientId',
+ clientSecret: 'clientSecret',
+ },
+
+ configurationUtilities
+ );
+ });
+
+ test('gets access token if connectorTokenClient is not provided', async () => {
+ (requestOAuthClientCredentialsToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthClientCredentialsAccessToken({
+ connectorId: '123',
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ tenantId: 'tenantId',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ },
+ },
+ oAuthScope: 'https://graph.microsoft.com/.default',
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ });
+
+ expect(connectorTokenClient.get).not.toHaveBeenCalled();
+ expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled();
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(requestOAuthClientCredentialsToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ logger,
+ {
+ scope: 'https://graph.microsoft.com/.default',
+ clientId: 'clientId',
+ clientSecret: 'clientSecret',
+ },
+
+ configurationUtilities
+ );
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts
new file mode 100644
index 0000000000000..803cce2db7668
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_client_credentials_access_token.ts
@@ -0,0 +1,99 @@
+/*
+ * 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 '@kbn/core/server';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { ConnectorToken, ConnectorTokenClientContract } from '../../types';
+import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token';
+
+export interface GetOAuthClientCredentialsConfig {
+ clientId: string;
+ tenantId: string;
+}
+
+export interface GetOAuthClientCredentialsSecrets {
+ clientSecret: string;
+}
+
+interface GetOAuthClientCredentialsAccessTokenOpts {
+ connectorId?: string;
+ tokenUrl: string;
+ oAuthScope: string;
+ logger: Logger;
+ configurationUtilities: ActionsConfigurationUtilities;
+ credentials: {
+ config: GetOAuthClientCredentialsConfig;
+ secrets: GetOAuthClientCredentialsSecrets;
+ };
+ connectorTokenClient?: ConnectorTokenClientContract;
+}
+
+export const getOAuthClientCredentialsAccessToken = async ({
+ connectorId,
+ logger,
+ tokenUrl,
+ oAuthScope,
+ configurationUtilities,
+ credentials,
+ connectorTokenClient,
+}: GetOAuthClientCredentialsAccessTokenOpts) => {
+ const { clientId, tenantId } = credentials.config;
+ const { clientSecret } = credentials.secrets;
+
+ if (!clientId || !clientSecret || !tenantId) {
+ logger.warn(`Missing required fields for requesting OAuth Client Credentials access token`);
+ return null;
+ }
+
+ let accessToken: string;
+ let connectorToken: ConnectorToken | null = null;
+ let hasErrors: boolean = false;
+
+ if (connectorId && connectorTokenClient) {
+ // Check if there is a token stored for this connector
+ const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({
+ connectorId,
+ });
+ connectorToken = token;
+ hasErrors = errors;
+ }
+
+ if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) {
+ // request access token with jwt assertion
+ const tokenResult = await requestOAuthClientCredentialsToken(
+ tokenUrl,
+ logger,
+ {
+ scope: oAuthScope,
+ clientId,
+ clientSecret,
+ },
+ configurationUtilities
+ );
+ accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`;
+
+ // try to update connector_token SO
+ if (connectorId && connectorTokenClient) {
+ try {
+ await connectorTokenClient.updateOrReplace({
+ connectorId,
+ token: connectorToken,
+ newToken: accessToken,
+ expiresInSec: tokenResult.expiresIn,
+ deleteExisting: hasErrors,
+ });
+ } catch (err) {
+ logger.warn(
+ `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}`
+ );
+ }
+ }
+ } else {
+ // use existing valid token
+ accessToken = connectorToken.token;
+ }
+ return accessToken;
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts
new file mode 100644
index 0000000000000..b48456ddd2a8c
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.test.ts
@@ -0,0 +1,350 @@
+/*
+ * 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 '@kbn/core/server';
+import { asyncForEach } from '@kbn/std';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
+import { actionsConfigMock } from '../../actions_config.mock';
+import { connectorTokenClientMock } from './connector_token_client.mock';
+import { getOAuthJwtAccessToken } from './get_oauth_jwt_access_token';
+import { createJWTAssertion } from './create_jwt_assertion';
+import { requestOAuthJWTToken } from './request_oauth_jwt_token';
+
+jest.mock('./create_jwt_assertion', () => ({
+ createJWTAssertion: jest.fn(),
+}));
+jest.mock('./request_oauth_jwt_token', () => ({
+ requestOAuthJWTToken: jest.fn(),
+}));
+
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+const configurationUtilities = actionsConfigMock.create();
+const connectorTokenClient = connectorTokenClientMock.create();
+
+describe('getOAuthJwtAccessToken', () => {
+ const getOAuthJwtAccessTokenOpts = {
+ connectorId: '123',
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'userIdentifierValue',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ privateKey: 'privateKey',
+ privateKeyPassword: 'privateKeyPassword',
+ },
+ },
+ tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do',
+ connectorTokenClient,
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ jest.clearAllMocks();
+ });
+
+ test('uses stored access token if it exists', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: {
+ id: '1',
+ connectorId: '123',
+ tokenType: 'access_token',
+ token: 'testtokenvalue',
+ createdAt: new Date().toISOString(),
+ expiresAt: new Date(Date.now() + 10000000000).toISOString(),
+ },
+ });
+ const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts);
+
+ expect(accessToken).toEqual('testtokenvalue');
+ expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled();
+ expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled();
+ });
+
+ test('creates new assertion if stored access token does not exist', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
+ (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts);
+
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
+ logger,
+ 'privateKey',
+ 'privateKeyPassword',
+ {
+ audience: 'clientId',
+ issuer: 'clientId',
+ subject: 'userIdentifierValue',
+ keyId: 'jwtKeyId',
+ }
+ );
+ expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://dev23432523.service-now.com/oauth_token.do',
+ { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
+ logger,
+ configurationUtilities
+ );
+ expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
+ connectorId: '123',
+ token: null,
+ newToken: 'access_token brandnewaccesstoken',
+ expiresInSec: 1000,
+ deleteExisting: false,
+ });
+ });
+
+ test('creates new assertion if stored access token exists but is expired', async () => {
+ const createdAt = new Date().toISOString();
+ const expiresAt = new Date(Date.now() - 100).toISOString();
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: {
+ id: '1',
+ connectorId: '123',
+ tokenType: 'access_token',
+ token: 'testtokenvalue',
+ createdAt,
+ expiresAt,
+ },
+ });
+ (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
+ (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts);
+
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
+ logger,
+ 'privateKey',
+ 'privateKeyPassword',
+ {
+ audience: 'clientId',
+ issuer: 'clientId',
+ subject: 'userIdentifierValue',
+ keyId: 'jwtKeyId',
+ }
+ );
+ expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://dev23432523.service-now.com/oauth_token.do',
+ { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
+ logger,
+ configurationUtilities
+ );
+ expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
+ connectorId: '123',
+ token: {
+ id: '1',
+ connectorId: '123',
+ tokenType: 'access_token',
+ token: 'testtokenvalue',
+ createdAt,
+ expiresAt,
+ },
+ newToken: 'access_token brandnewaccesstoken',
+ expiresInSec: 1000,
+ deleteExisting: false,
+ });
+ });
+
+ test('returns null and logs warning if any required fields are missing', async () => {
+ await asyncForEach(
+ ['clientId', 'jwtKeyId', 'userIdentifierValue'],
+ async (configField: string) => {
+ const accessToken = await getOAuthJwtAccessToken({
+ ...getOAuthJwtAccessTokenOpts,
+ credentials: {
+ config: { ...getOAuthJwtAccessTokenOpts.credentials.config, [configField]: null },
+ secrets: getOAuthJwtAccessTokenOpts.credentials.secrets,
+ },
+ });
+ expect(accessToken).toBeNull();
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Missing required fields for requesting OAuth JWT access token`
+ );
+ }
+ );
+
+ await asyncForEach(['clientSecret', 'privateKey'], async (secretsField: string) => {
+ const accessToken = await getOAuthJwtAccessToken({
+ ...getOAuthJwtAccessTokenOpts,
+ credentials: {
+ config: getOAuthJwtAccessTokenOpts.credentials.config,
+ secrets: { ...getOAuthJwtAccessTokenOpts.credentials.secrets, [secretsField]: null },
+ },
+ });
+ expect(accessToken).toBeNull();
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Missing required fields for requesting OAuth JWT access token`
+ );
+ });
+ });
+
+ test('throws error if createJWTAssertion throws error', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (createJWTAssertion as jest.Mock).mockImplementationOnce(() => {
+ throw new Error('createJWTAssertion error!!');
+ });
+
+ await expect(
+ getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"createJWTAssertion error!!"`);
+ });
+
+ test('throws error if requestOAuthJWTToken throws error', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
+ (requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce(
+ new Error('requestOAuthJWTToken error!!')
+ );
+
+ await expect(
+ getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"requestOAuthJWTToken error!!"`);
+ });
+
+ test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => {
+ connectorTokenClient.get.mockResolvedValueOnce({
+ hasErrors: false,
+ connectorToken: null,
+ });
+ (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
+ (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+ connectorTokenClient.updateOrReplace.mockRejectedValueOnce(new Error('updateOrReplace error'));
+
+ const accessToken = await getOAuthJwtAccessToken(getOAuthJwtAccessTokenOpts);
+
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(logger.warn).toHaveBeenCalledWith(
+ `Not able to update connector token for connectorId: 123 due to error: updateOrReplace error`
+ );
+ });
+
+ test('gets access token if connectorId is not provided', async () => {
+ (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
+ (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthJwtAccessToken({
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'userIdentifierValue',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ privateKey: 'privateKey',
+ privateKeyPassword: 'privateKeyPassword',
+ },
+ },
+ tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do',
+ connectorTokenClient,
+ });
+
+ expect(connectorTokenClient.get).not.toHaveBeenCalled();
+ expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled();
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
+ logger,
+ 'privateKey',
+ 'privateKeyPassword',
+ {
+ audience: 'clientId',
+ issuer: 'clientId',
+ subject: 'userIdentifierValue',
+ keyId: 'jwtKeyId',
+ }
+ );
+ expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://dev23432523.service-now.com/oauth_token.do',
+ { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
+ logger,
+ configurationUtilities
+ );
+ });
+
+ test('gets access token if connectorTokenClient is not provided', async () => {
+ (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
+ (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
+ tokenType: 'access_token',
+ accessToken: 'brandnewaccesstoken',
+ expiresIn: 1000,
+ });
+
+ const accessToken = await getOAuthJwtAccessToken({
+ connectorId: '123',
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'userIdentifierValue',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ privateKey: 'privateKey',
+ privateKeyPassword: 'privateKeyPassword',
+ },
+ },
+ tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do',
+ });
+
+ expect(connectorTokenClient.get).not.toHaveBeenCalled();
+ expect(connectorTokenClient.updateOrReplace).not.toHaveBeenCalled();
+ expect(accessToken).toEqual('access_token brandnewaccesstoken');
+ expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
+ logger,
+ 'privateKey',
+ 'privateKeyPassword',
+ {
+ audience: 'clientId',
+ issuer: 'clientId',
+ subject: 'userIdentifierValue',
+ keyId: 'jwtKeyId',
+ }
+ );
+ expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
+ 'https://dev23432523.service-now.com/oauth_token.do',
+ { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
+ logger,
+ configurationUtilities
+ );
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.ts
new file mode 100644
index 0000000000000..a4867d99556e7
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_oauth_jwt_access_token.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 } from '@kbn/core/server';
+import { ActionsConfigurationUtilities } from '../../actions_config';
+import { ConnectorToken, ConnectorTokenClientContract } from '../../types';
+import { createJWTAssertion } from './create_jwt_assertion';
+import { requestOAuthJWTToken } from './request_oauth_jwt_token';
+
+export interface GetOAuthJwtConfig {
+ clientId: string;
+ jwtKeyId: string;
+ userIdentifierValue: string;
+}
+
+export interface GetOAuthJwtSecrets {
+ clientSecret: string;
+ privateKey: string;
+ privateKeyPassword: string | null;
+}
+
+interface GetOAuthJwtAccessTokenOpts {
+ connectorId?: string;
+ tokenUrl: string;
+ logger: Logger;
+ configurationUtilities: ActionsConfigurationUtilities;
+ credentials: {
+ config: GetOAuthJwtConfig;
+ secrets: GetOAuthJwtSecrets;
+ };
+ connectorTokenClient?: ConnectorTokenClientContract;
+}
+
+export const getOAuthJwtAccessToken = async ({
+ connectorId,
+ logger,
+ tokenUrl,
+ configurationUtilities,
+ credentials,
+ connectorTokenClient,
+}: GetOAuthJwtAccessTokenOpts) => {
+ const { clientId, jwtKeyId, userIdentifierValue } = credentials.config;
+ const { clientSecret, privateKey, privateKeyPassword } = credentials.secrets;
+
+ if (!clientId || !clientSecret || !jwtKeyId || !privateKey || !userIdentifierValue) {
+ logger.warn(`Missing required fields for requesting OAuth JWT access token`);
+ return null;
+ }
+
+ let accessToken: string;
+ let connectorToken: ConnectorToken | null = null;
+ let hasErrors: boolean = false;
+
+ if (connectorId && connectorTokenClient) {
+ // Check if there is a token stored for this connector
+ const { connectorToken: token, hasErrors: errors } = await connectorTokenClient.get({
+ connectorId,
+ });
+ connectorToken = token;
+ hasErrors = errors;
+ }
+
+ if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) {
+ // generate a new assertion
+ const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, {
+ audience: clientId,
+ issuer: clientId,
+ subject: userIdentifierValue,
+ keyId: jwtKeyId,
+ });
+
+ // request access token with jwt assertion
+ const tokenResult = await requestOAuthJWTToken(
+ tokenUrl,
+ {
+ clientId,
+ clientSecret,
+ assertion,
+ },
+ logger,
+ configurationUtilities
+ );
+ accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`;
+
+ // try to update connector_token SO
+ if (connectorId && connectorTokenClient) {
+ try {
+ await connectorTokenClient.updateOrReplace({
+ connectorId,
+ token: connectorToken,
+ newToken: accessToken,
+ expiresInSec: tokenResult.expiresIn,
+ deleteExisting: hasErrors,
+ });
+ } catch (err) {
+ logger.warn(
+ `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}`
+ );
+ }
+ }
+ } else {
+ // use existing valid token
+ accessToken = connectorToken.token;
+ }
+ return accessToken;
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
index 1d1c2c46cb0e4..fbf0d90541659 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts
@@ -12,8 +12,8 @@ jest.mock('nodemailer', () => ({
jest.mock('./send_email_graph_api', () => ({
sendEmailGraphApi: jest.fn(),
}));
-jest.mock('./request_oauth_client_credentials_token', () => ({
- requestOAuthClientCredentialsToken: jest.fn(),
+jest.mock('./get_oauth_client_credentials_access_token', () => ({
+ getOAuthClientCredentialsAccessToken: jest.fn(),
}));
import { Logger } from '@kbn/core/server';
@@ -24,10 +24,9 @@ import { ProxySettings } from '../../types';
import { actionsConfigMock } from '../../actions_config.mock';
import { CustomHostSettings } from '../../config';
import { sendEmailGraphApi } from './send_email_graph_api';
-import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token';
+import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token';
import { ConnectorTokenClient } from './connector_token_client';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
-import { connectorTokenClientMock } from './connector_token_client.mock';
const createTransportMock = nodemailer.createTransport as jest.Mock;
const sendMailMockResult = { result: 'does not matter' };
@@ -92,314 +91,38 @@ describe('send_email module', () => {
test('uses OAuth 2.0 Client Credentials authentication for email using "exchange_server" service', async () => {
const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock;
- const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock;
+ const getOAuthClientCredentialsAccessTokenMock =
+ getOAuthClientCredentialsAccessToken as jest.Mock;
const sendEmailOptions = getSendEmailOptions({
transport: {
service: 'exchange_server',
clientId: '123456',
+ tenantId: '98765',
clientSecret: 'sdfhkdsjhfksdjfh',
},
});
- requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({
- tokenType: 'Bearer',
- accessToken: 'dfjsdfgdjhfgsjdf',
- expiresIn: 123,
- });
+ getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(`Bearer dfjsdfgdjhfgsjdf`);
const date = new Date();
date.setDate(date.getDate() + 5);
- unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
- id: '1',
- type: 'connector_token',
- references: [],
- attributes: {
- connectorId: '123',
- expiresAt: date.toISOString(),
- tokenType: 'access_token',
- token: '11111111',
- },
- });
-
sendEmailGraphApiMock.mockReturnValue({
status: 202,
});
- unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
- total: 0,
- saved_objects: [],
- per_page: 500,
- page: 1,
- });
await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
- requestOAuthClientCredentialsTokenMock.mock.calls[0].pop();
- expect(requestOAuthClientCredentialsTokenMock.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- "https://login.microsoftonline.com/undefined/oauth2/v2.0/token",
- Object {
- "context": Array [],
- "debug": [MockFunction],
- "error": [MockFunction],
- "fatal": [MockFunction],
- "get": [MockFunction],
- "info": [MockFunction],
- "log": [MockFunction],
- "trace": [MockFunction],
- "warn": [MockFunction],
- },
- Object {
- "clientId": "123456",
- "clientSecret": "sdfhkdsjhfksdjfh",
- "scope": "https://graph.microsoft.com/.default",
- },
- ]
- `);
-
- delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
- sendEmailGraphApiMock.mock.calls[0].pop();
- expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "graphApiUrl": undefined,
- "headers": Object {
- "Authorization": "Bearer dfjsdfgdjhfgsjdf",
- "Content-Type": "application/json",
- },
- "messageHTML": "a message
- ",
- "options": Object {
- "connectorId": "1",
- "content": Object {
- "message": "a message",
- "subject": "a subject",
- },
- "hasAuth": true,
- "routing": Object {
- "bcc": Array [],
- "cc": Array [
- "bob@example.com",
- "robert@example.com",
- ],
- "from": "fred@example.com",
- "to": Array [
- "jim@example.com",
- ],
- },
- "transport": Object {
- "clientId": "123456",
- "clientSecret": "sdfhkdsjhfksdjfh",
- "password": "changeme",
- "service": "exchange_server",
- "user": "elastic",
- },
- },
- },
- Object {
- "context": Array [],
- "debug": [MockFunction],
- "error": [MockFunction],
- "fatal": [MockFunction],
- "get": [MockFunction],
- "info": [MockFunction],
- "log": [MockFunction],
- "trace": [MockFunction],
- "warn": [MockFunction],
- },
- ]
- `);
-
- expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1);
- });
-
- test('uses existing "access_token" from "connector_token" SO for authentication for email using "exchange_server" service', async () => {
- const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock;
- const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock;
- const sendEmailOptions = getSendEmailOptions({
- transport: {
- service: 'exchange_server',
- clientId: '123456',
- clientSecret: 'sdfhkdsjhfksdjfh',
- },
- });
- requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({
- tokenType: 'Bearer',
- accessToken: 'dfjsdfgdjhfgsjdf',
- expiresIn: 123,
- });
-
- sendEmailGraphApiMock.mockReturnValue({
- status: 202,
- });
- const date = new Date();
- date.setDate(date.getDate() + 5);
-
- unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({
- errors: [],
- });
- unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
- total: 2,
- saved_objects: [
- {
- id: '1',
- score: 1,
- type: 'connector_token',
- references: [],
- attributes: {
- connectorId: '123',
- expiresAt: date.toISOString(),
- tokenType: 'access_token',
- },
- },
- ],
- per_page: 500,
- page: 1,
- });
- encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
- id: '1',
- type: 'connector_token',
- references: [],
- attributes: {
- token: '11111111',
+ expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({
+ configurationUtilities: sendEmailOptions.configurationUtilities,
+ connectorId: '1',
+ connectorTokenClient,
+ credentials: {
+ config: { clientId: '123456', tenantId: '98765' },
+ secrets: { clientSecret: 'sdfhkdsjhfksdjfh' },
},
+ logger: mockLogger,
+ oAuthScope: 'https://graph.microsoft.com/.default',
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
});
- await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
- expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(0);
-
- delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
- sendEmailGraphApiMock.mock.calls[0].pop();
- expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "graphApiUrl": undefined,
- "headers": Object {
- "Authorization": "11111111",
- "Content-Type": "application/json",
- },
- "messageHTML": "a message
- ",
- "options": Object {
- "connectorId": "1",
- "content": Object {
- "message": "a message",
- "subject": "a subject",
- },
- "hasAuth": true,
- "routing": Object {
- "bcc": Array [],
- "cc": Array [
- "bob@example.com",
- "robert@example.com",
- ],
- "from": "fred@example.com",
- "to": Array [
- "jim@example.com",
- ],
- },
- "transport": Object {
- "clientId": "123456",
- "clientSecret": "sdfhkdsjhfksdjfh",
- "password": "changeme",
- "service": "exchange_server",
- "user": "elastic",
- },
- },
- },
- Object {
- "context": Array [],
- "debug": [MockFunction],
- "error": [MockFunction],
- "fatal": [MockFunction],
- "get": [MockFunction],
- "info": [MockFunction],
- "log": [MockFunction],
- "trace": [MockFunction],
- "warn": [MockFunction],
- },
- ]
- `);
-
- expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(0);
- });
-
- test('request the new token and update existing "access_token" when it is expired for "exchange_server" email service', async () => {
- const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock;
- const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock;
- const sendEmailOptions = getSendEmailOptions({
- transport: {
- service: 'exchange_server',
- clientId: '123456',
- clientSecret: 'sdfhkdsjhfksdjfh',
- },
- });
- requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({
- tokenType: 'Bearer',
- accessToken: 'dfjsdfgdjhfgsjdf',
- expiresIn: 123,
- });
-
- sendEmailGraphApiMock.mockReturnValue({
- status: 202,
- });
- const date = new Date();
- date.setDate(date.getDate() - 5);
-
- unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
- total: 2,
- saved_objects: [
- {
- id: '1',
- score: 1,
- type: 'connector_token',
- references: [],
- attributes: {
- connectorId: '123',
- expiresAt: date.toISOString(),
- tokenType: 'access_token',
- },
- },
- ],
- per_page: 500,
- page: 1,
- });
- encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({
- id: '1',
- type: 'connector_token',
- references: [],
- attributes: {
- token: '11111111',
- },
- });
-
- unsecuredSavedObjectsClient.get.mockResolvedValueOnce({
- id: '1',
- type: 'connector_token',
- references: [],
- attributes: {
- connectorId: '123',
- expiresAt: date.toISOString(),
- tokenType: 'access_token',
- token: '11111111',
- },
- });
- unsecuredSavedObjectsClient.checkConflicts.mockResolvedValueOnce({
- errors: [],
- });
-
- unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
- id: '1',
- type: 'connector_token',
- references: [],
- attributes: {
- connectorId: '123',
- expiresAt: date.toISOString(),
- tokenType: 'access_token',
- token: '11111111',
- },
- });
-
- await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
- expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1);
-
delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
sendEmailGraphApiMock.mock.calls[0].pop();
expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(`
@@ -435,6 +158,7 @@ describe('send_email module', () => {
"clientSecret": "sdfhkdsjhfksdjfh",
"password": "changeme",
"service": "exchange_server",
+ "tenantId": "98765",
"user": "elastic",
},
},
@@ -452,209 +176,42 @@ describe('send_email module', () => {
},
]
`);
-
- expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1);
});
- test('sending email for "exchange_server" wont fail if connectorTokenClient throw the errors, just log warning message', async () => {
+ test('throws error if null access token returned when using OAuth 2.0 Client Credentials authentication', async () => {
const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock;
- const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock;
+ const getOAuthClientCredentialsAccessTokenMock =
+ getOAuthClientCredentialsAccessToken as jest.Mock;
const sendEmailOptions = getSendEmailOptions({
transport: {
service: 'exchange_server',
clientId: '123456',
+ tenantId: '98765',
clientSecret: 'sdfhkdsjhfksdjfh',
},
});
- requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({
- tokenType: 'Bearer',
- accessToken: 'dfjsdfgdjhfgsjdf',
- expiresIn: 123,
- });
-
- sendEmailGraphApiMock.mockReturnValue({
- status: 202,
- });
- const date = new Date();
- date.setDate(date.getDate() + 5);
-
- unsecuredSavedObjectsClient.find.mockResolvedValueOnce({
- total: 0,
- saved_objects: [],
- per_page: 500,
- page: 1,
- });
- unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
-
- await sendEmail(mockLogger, sendEmailOptions, connectorTokenClient);
- expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1);
- expect(unsecuredSavedObjectsClient.create.mock.calls.length).toBe(1);
- expect(mockLogger.warn.mock.calls[0]).toMatchObject([
- `Not able to update connector token for connectorId: 1 due to error: Fail`,
- ]);
+ getOAuthClientCredentialsAccessTokenMock.mockReturnValueOnce(null);
- delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
- sendEmailGraphApiMock.mock.calls[0].pop();
- expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "graphApiUrl": undefined,
- "headers": Object {
- "Authorization": "Bearer dfjsdfgdjhfgsjdf",
- "Content-Type": "application/json",
- },
- "messageHTML": "a message
- ",
- "options": Object {
- "connectorId": "1",
- "content": Object {
- "message": "a message",
- "subject": "a subject",
- },
- "hasAuth": true,
- "routing": Object {
- "bcc": Array [],
- "cc": Array [
- "bob@example.com",
- "robert@example.com",
- ],
- "from": "fred@example.com",
- "to": Array [
- "jim@example.com",
- ],
- },
- "transport": Object {
- "clientId": "123456",
- "clientSecret": "sdfhkdsjhfksdjfh",
- "password": "changeme",
- "service": "exchange_server",
- "user": "elastic",
- },
- },
- },
- Object {
- "context": Array [],
- "debug": [MockFunction],
- "error": [MockFunction] {
- "calls": Array [
- Array [
- "Failed to create connector_token for connectorId \\"1\\" and tokenType: \\"access_token\\". Error: Fail",
- ],
- ],
- "results": Array [
- Object {
- "type": "return",
- "value": undefined,
- },
- ],
- },
- "fatal": [MockFunction],
- "get": [MockFunction],
- "info": [MockFunction],
- "log": [MockFunction],
- "trace": [MockFunction],
- "warn": [MockFunction] {
- "calls": Array [
- Array [
- "Not able to update connector token for connectorId: 1 due to error: Fail",
- ],
- ],
- "results": Array [
- Object {
- "type": "return",
- "value": undefined,
- },
- ],
- },
- },
- ]
- `);
- });
+ await expect(() =>
+ sendEmail(mockLogger, sendEmailOptions, connectorTokenClient)
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"Unable to retrieve access token for connectorId: 1"`
+ );
- test('delete duplication tokens if connectorTokenClient get method has the errors, like decription error', async () => {
- const sendEmailGraphApiMock = sendEmailGraphApi as jest.Mock;
- const requestOAuthClientCredentialsTokenMock = requestOAuthClientCredentialsToken as jest.Mock;
- const sendEmailOptions = getSendEmailOptions({
- transport: {
- service: 'exchange_server',
- clientId: '123456',
- clientSecret: 'sdfhkdsjhfksdjfh',
+ expect(getOAuthClientCredentialsAccessTokenMock).toHaveBeenCalledWith({
+ configurationUtilities: sendEmailOptions.configurationUtilities,
+ connectorId: '1',
+ connectorTokenClient,
+ credentials: {
+ config: { clientId: '123456', tenantId: '98765' },
+ secrets: { clientSecret: 'sdfhkdsjhfksdjfh' },
},
+ logger: mockLogger,
+ oAuthScope: 'https://graph.microsoft.com/.default',
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
});
- requestOAuthClientCredentialsTokenMock.mockReturnValueOnce({
- tokenType: 'Bearer',
- accessToken: 'dfjsdfgdjhfgsjdf',
- expiresIn: 123,
- });
-
- sendEmailGraphApiMock.mockReturnValue({
- status: 202,
- });
- const date = new Date();
- date.setDate(date.getDate() + 5);
-
- const connectorTokenClientM = connectorTokenClientMock.create();
- connectorTokenClientM.get.mockResolvedValueOnce({
- hasErrors: true,
- connectorToken: null,
- });
-
- await sendEmail(mockLogger, sendEmailOptions, connectorTokenClientM);
- expect(requestOAuthClientCredentialsTokenMock.mock.calls.length).toBe(1);
- expect(connectorTokenClientM.updateOrReplace.mock.calls.length).toBe(1);
- delete sendEmailGraphApiMock.mock.calls[0][0].options.configurationUtilities;
- sendEmailGraphApiMock.mock.calls[0].pop();
- expect(sendEmailGraphApiMock.mock.calls[0]).toMatchInlineSnapshot(`
- Array [
- Object {
- "graphApiUrl": undefined,
- "headers": Object {
- "Authorization": "Bearer dfjsdfgdjhfgsjdf",
- "Content-Type": "application/json",
- },
- "messageHTML": "a message
- ",
- "options": Object {
- "connectorId": "1",
- "content": Object {
- "message": "a message",
- "subject": "a subject",
- },
- "hasAuth": true,
- "routing": Object {
- "bcc": Array [],
- "cc": Array [
- "bob@example.com",
- "robert@example.com",
- ],
- "from": "fred@example.com",
- "to": Array [
- "jim@example.com",
- ],
- },
- "transport": Object {
- "clientId": "123456",
- "clientSecret": "sdfhkdsjhfksdjfh",
- "password": "changeme",
- "service": "exchange_server",
- "user": "elastic",
- },
- },
- },
- Object {
- "context": Array [],
- "debug": [MockFunction],
- "error": [MockFunction],
- "fatal": [MockFunction],
- "get": [MockFunction],
- "info": [MockFunction],
- "log": [MockFunction],
- "trace": [MockFunction],
- "warn": [MockFunction],
- },
- ]
- `);
+ expect(sendEmailGraphApiMock).not.toHaveBeenCalled();
});
test('handles unauthenticated email using not secure host/port', async () => {
diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
index 983846adc71e0..f2b059e51e0d6 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts
@@ -14,9 +14,9 @@ import { ActionsConfigurationUtilities } from '../../actions_config';
import { CustomHostSettings } from '../../config';
import { getNodeSSLOptions, getSSLSettingsFromConfig } from './get_node_ssl_options';
import { sendEmailGraphApi } from './send_email_graph_api';
-import { requestOAuthClientCredentialsToken } from './request_oauth_client_credentials_token';
import { ConnectorTokenClientContract, ProxySettings } from '../../types';
import { AdditionalEmailServices } from '../../../common';
+import { getOAuthClientCredentialsAccessToken } from './get_oauth_client_credentials_access_token';
// an email "service" which doesn't actually send, just returns what it would send
export const JSON_TRANSPORT_SERVICE = '__json';
@@ -86,41 +86,28 @@ async function sendEmailWithExchange(
const { transport, configurationUtilities, connectorId } = options;
const { clientId, clientSecret, tenantId, oauthTokenUrl } = transport;
- let accessToken: string;
-
- const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId });
- if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) {
- // request new access token for microsoft exchange online server with Graph API scope
- const tokenResult = await requestOAuthClientCredentialsToken(
- oauthTokenUrl ?? `${EXCHANGE_ONLINE_SERVER_HOST}/${tenantId}/oauth2/v2.0/token`,
- logger,
- {
- scope: GRAPH_API_OAUTH_SCOPE,
- clientId,
- clientSecret,
+ const accessToken = await getOAuthClientCredentialsAccessToken({
+ connectorId,
+ logger,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: clientId as string,
+ tenantId: tenantId as string,
+ },
+ secrets: {
+ clientSecret: clientSecret as string,
},
- configurationUtilities
- );
- accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`;
+ },
+ oAuthScope: GRAPH_API_OAUTH_SCOPE,
+ tokenUrl: oauthTokenUrl ?? `${EXCHANGE_ONLINE_SERVER_HOST}/${tenantId}/oauth2/v2.0/token`,
+ connectorTokenClient,
+ });
- // try to update connector_token SO
- try {
- await connectorTokenClient.updateOrReplace({
- connectorId,
- token: connectorToken,
- newToken: accessToken,
- expiresInSec: tokenResult.expiresIn,
- deleteExisting: hasErrors,
- });
- } catch (err) {
- logger.warn(
- `Not able to update connector token for connectorId: ${connectorId} due to error: ${err.message}`
- );
- }
- } else {
- // use existing valid token
- accessToken = connectorToken.token;
+ if (!accessToken) {
+ throw new Error(`Unable to retrieve access token for connectorId: ${connectorId}`);
}
+
const headers = {
'Content-Type': 'application/json',
Authorization: accessToken,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts
index dae4e59728a0c..64a80977709e5 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts
@@ -14,19 +14,14 @@ import {
createServiceError,
getPushedDate,
throwIfSubActionIsNotSupported,
- getAccessToken,
getAxiosInstance,
} from './utils';
import { connectorTokenClientMock } from '../lib/connector_token_client.mock';
import { actionsConfigMock } from '../../actions_config.mock';
-import { createJWTAssertion } from '../lib/create_jwt_assertion';
-import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token';
+import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token';
-jest.mock('../lib/create_jwt_assertion', () => ({
- createJWTAssertion: jest.fn(),
-}));
-jest.mock('../lib/request_oauth_jwt_token', () => ({
- requestOAuthJWTToken: jest.fn(),
+jest.mock('../lib/get_oauth_jwt_access_token', () => ({
+ getOAuthJwtAccessToken: jest.fn(),
}));
jest.mock('axios', () => ({
@@ -195,7 +190,7 @@ describe('utils', () => {
});
});
- test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', () => {
+ test('creates axios instance with interceptor when isOAuth is true and OAuth fields are defined', async () => {
connectorTokenClient.get.mockResolvedValueOnce({
hasErrors: false,
connectorToken: {
@@ -235,206 +230,34 @@ describe('utils', () => {
expect(createAxiosInstanceMock).toHaveBeenCalledTimes(1);
expect(createAxiosInstanceMock).toHaveBeenCalledWith();
expect(axiosInstanceMock.interceptors.request.use).toHaveBeenCalledTimes(1);
- });
- });
- describe('getAccessToken', () => {
- const getAccessTokenOpts = {
- connectorId: '123',
- logger,
- configurationUtilities,
- credentials: {
- config: {
- apiUrl: 'https://servicenow',
- usesTableApi: true,
- isOAuth: true,
- clientId: 'clientId',
- jwtKeyId: 'jwtKeyId',
- userIdentifierValue: 'userIdentifierValue',
- },
- secrets: {
- clientSecret: 'clientSecret',
- privateKey: 'privateKey',
- privateKeyPassword: 'privateKeyPassword',
- username: null,
- password: null,
- },
- },
- snServiceUrl: 'https://dev23432523.service-now.com',
- connectorTokenClient,
- };
- beforeEach(() => {
- jest.resetAllMocks();
- jest.clearAllMocks();
- });
+ (getOAuthJwtAccessToken as jest.Mock).mockResolvedValueOnce('Bearer tokentokentoken');
- test('uses stored access token if it exists', async () => {
- connectorTokenClient.get.mockResolvedValueOnce({
- hasErrors: false,
- connectorToken: {
- id: '1',
- connectorId: '123',
- tokenType: 'access_token',
- token: 'testtokenvalue',
- createdAt: new Date().toISOString(),
- expiresAt: new Date(Date.now() + 10000000000).toISOString(),
- },
- });
- const accessToken = await getAccessToken(getAccessTokenOpts);
-
- expect(accessToken).toEqual('testtokenvalue');
- expect(createJWTAssertion as jest.Mock).not.toHaveBeenCalled();
- expect(requestOAuthJWTToken as jest.Mock).not.toHaveBeenCalled();
- });
-
- test('creates new assertion if stored access token does not exist', async () => {
- connectorTokenClient.get.mockResolvedValueOnce({
- hasErrors: false,
- connectorToken: null,
- });
- (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
- (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
- tokenType: 'access_token',
- accessToken: 'brandnewaccesstoken',
- expiresIn: 1000,
+ const mockRequestCallback = (axiosInstanceMock.interceptors.request.use as jest.Mock).mock
+ .calls[0][0];
+ expect(await mockRequestCallback({ headers: {} })).toEqual({
+ headers: { Authorization: 'Bearer tokentokentoken' },
});
- const accessToken = await getAccessToken(getAccessTokenOpts);
-
- expect(accessToken).toEqual('access_token brandnewaccesstoken');
- expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
- logger,
- 'privateKey',
- 'privateKeyPassword',
- {
- audience: 'clientId',
- issuer: 'clientId',
- subject: 'userIdentifierValue',
- keyId: 'jwtKeyId',
- }
- );
- expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
- 'https://dev23432523.service-now.com/oauth_token.do',
- { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
- logger,
- configurationUtilities
- );
- expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
+ expect(getOAuthJwtAccessToken as jest.Mock).toHaveBeenCalledWith({
connectorId: '123',
- token: null,
- newToken: 'access_token brandnewaccesstoken',
- expiresInSec: 1000,
- deleteExisting: false,
- });
- });
-
- test('creates new assertion if stored access token exists but is expired', async () => {
- const createdAt = new Date().toISOString();
- const expiresAt = new Date(Date.now() - 100).toISOString();
- connectorTokenClient.get.mockResolvedValueOnce({
- hasErrors: false,
- connectorToken: {
- id: '1',
- connectorId: '123',
- tokenType: 'access_token',
- token: 'testtokenvalue',
- createdAt,
- expiresAt,
- },
- });
- (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
- (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
- tokenType: 'access_token',
- accessToken: 'brandnewaccesstoken',
- expiresIn: 1000,
- });
-
- const accessToken = await getAccessToken(getAccessTokenOpts);
-
- expect(accessToken).toEqual('access_token brandnewaccesstoken');
- expect(createJWTAssertion as jest.Mock).toHaveBeenCalledWith(
- logger,
- 'privateKey',
- 'privateKeyPassword',
- {
- audience: 'clientId',
- issuer: 'clientId',
- subject: 'userIdentifierValue',
- keyId: 'jwtKeyId',
- }
- );
- expect(requestOAuthJWTToken as jest.Mock).toHaveBeenCalledWith(
- 'https://dev23432523.service-now.com/oauth_token.do',
- { clientId: 'clientId', clientSecret: 'clientSecret', assertion: 'newassertion' },
logger,
- configurationUtilities
- );
- expect(connectorTokenClient.updateOrReplace).toHaveBeenCalledWith({
- connectorId: '123',
- token: {
- id: '1',
- connectorId: '123',
- tokenType: 'access_token',
- token: 'testtokenvalue',
- createdAt,
- expiresAt,
+ configurationUtilities,
+ credentials: {
+ config: {
+ clientId: 'clientId',
+ jwtKeyId: 'jwtKeyId',
+ userIdentifierValue: 'userIdentifierValue',
+ },
+ secrets: {
+ clientSecret: 'clientSecret',
+ privateKey: 'privateKey',
+ privateKeyPassword: null,
+ },
},
- newToken: 'access_token brandnewaccesstoken',
- expiresInSec: 1000,
- deleteExisting: false,
- });
- });
-
- test('throws error if createJWTAssertion throws error', async () => {
- connectorTokenClient.get.mockResolvedValueOnce({
- hasErrors: false,
- connectorToken: null,
- });
- (createJWTAssertion as jest.Mock).mockImplementationOnce(() => {
- throw new Error('createJWTAssertion error!!');
- });
-
- await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot(
- `"createJWTAssertion error!!"`
- );
- });
-
- test('throws error if requestOAuthJWTToken throws error', async () => {
- connectorTokenClient.get.mockResolvedValueOnce({
- hasErrors: false,
- connectorToken: null,
- });
- (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
- (requestOAuthJWTToken as jest.Mock).mockRejectedValueOnce(
- new Error('requestOAuthJWTToken error!!')
- );
-
- await expect(getAccessToken(getAccessTokenOpts)).rejects.toThrowErrorMatchingInlineSnapshot(
- `"requestOAuthJWTToken error!!"`
- );
- });
-
- test('logs warning if connectorTokenClient.updateOrReplace throws error', async () => {
- connectorTokenClient.get.mockResolvedValueOnce({
- hasErrors: false,
- connectorToken: null,
- });
- (createJWTAssertion as jest.Mock).mockReturnValueOnce('newassertion');
- (requestOAuthJWTToken as jest.Mock).mockResolvedValueOnce({
- tokenType: 'access_token',
- accessToken: 'brandnewaccesstoken',
- expiresIn: 1000,
+ tokenUrl: 'https://dev23432523.service-now.com/oauth_token.do',
+ connectorTokenClient,
});
- connectorTokenClient.updateOrReplace.mockRejectedValueOnce(
- new Error('updateOrReplace error')
- );
-
- const accessToken = await getAccessToken(getAccessTokenOpts);
-
- expect(accessToken).toEqual('access_token brandnewaccesstoken');
- expect(logger.warn).toHaveBeenCalledWith(
- `Not able to update ServiceNow connector token for connectorId: 123 due to error: updateOrReplace error`
- );
});
});
});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts
index 84d6741398bce..538967269b1ea 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts
@@ -21,8 +21,7 @@ import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils';
import * as i18n from './translations';
import { ActionsConfigurationUtilities } from '../../actions_config';
import { ConnectorTokenClientContract } from '../../types';
-import { createJWTAssertion } from '../lib/create_jwt_assertion';
-import { requestOAuthJWTToken } from '../lib/request_oauth_jwt_token';
+import { getOAuthJwtAccessToken } from '../lib/get_oauth_jwt_access_token';
export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident =>
useOldApi
@@ -83,13 +82,13 @@ export const throwIfSubActionIsNotSupported = ({
}
};
-export interface GetAccessTokenAndAxiosInstanceOpts {
- connectorId: string;
+export interface GetAxiosInstanceOpts {
+ connectorId?: string;
logger: Logger;
configurationUtilities: ActionsConfigurationUtilities;
credentials: ExternalServiceCredentials;
snServiceUrl: string;
- connectorTokenClient: ConnectorTokenClientContract;
+ connectorTokenClient?: ConnectorTokenClientContract;
}
export const getAxiosInstance = ({
@@ -99,7 +98,7 @@ export const getAxiosInstance = ({
credentials,
snServiceUrl,
connectorTokenClient,
-}: GetAccessTokenAndAxiosInstanceOpts): AxiosInstance => {
+}: GetAxiosInstanceOpts): AxiosInstance => {
const { config, secrets } = credentials;
const { isOAuth } = config as ServiceNowPublicConfigurationType;
const { username, password } = secrets as ServiceNowSecretConfigurationType;
@@ -114,15 +113,25 @@ export const getAxiosInstance = ({
axiosInstance = axios.create();
axiosInstance.interceptors.request.use(
async (axiosConfig: AxiosRequestConfig) => {
- const accessToken = await getAccessToken({
+ const accessToken = await getOAuthJwtAccessToken({
connectorId,
logger,
configurationUtilities,
credentials: {
- config: config as ServiceNowPublicConfigurationType,
- secrets,
+ config: {
+ clientId: config.clientId as string,
+ jwtKeyId: config.jwtKeyId as string,
+ userIdentifierValue: config.userIdentifierValue as string,
+ },
+ secrets: {
+ clientSecret: secrets.clientSecret as string,
+ privateKey: secrets.privateKey as string,
+ privateKeyPassword: secrets.privateKeyPassword
+ ? (secrets.privateKeyPassword as string)
+ : null,
+ },
},
- snServiceUrl,
+ tokenUrl: `${snServiceUrl}/oauth_token.do`,
connectorTokenClient,
});
axiosConfig.headers.Authorization = accessToken;
@@ -136,75 +145,3 @@ export const getAxiosInstance = ({
return axiosInstance;
};
-
-export const getAccessToken = async ({
- connectorId,
- logger,
- configurationUtilities,
- credentials,
- snServiceUrl,
- connectorTokenClient,
-}: GetAccessTokenAndAxiosInstanceOpts) => {
- const { isOAuth, clientId, jwtKeyId, userIdentifierValue } =
- credentials.config as ServiceNowPublicConfigurationType;
- const { clientSecret, privateKey, privateKeyPassword } =
- credentials.secrets as ServiceNowSecretConfigurationType;
-
- let accessToken: string;
-
- // Check if there is a token stored for this connector
- const { connectorToken, hasErrors } = await connectorTokenClient.get({ connectorId });
-
- if (connectorToken === null || Date.parse(connectorToken.expiresAt) <= Date.now()) {
- // generate a new assertion
- if (
- !isOAuth ||
- !clientId ||
- !clientSecret ||
- !jwtKeyId ||
- !privateKey ||
- !userIdentifierValue
- ) {
- return null;
- }
-
- const assertion = createJWTAssertion(logger, privateKey, privateKeyPassword, {
- audience: clientId,
- issuer: clientId,
- subject: userIdentifierValue,
- keyId: jwtKeyId,
- });
-
- // request access token with jwt assertion
- const tokenResult = await requestOAuthJWTToken(
- `${snServiceUrl}/oauth_token.do`,
- {
- clientId,
- clientSecret,
- assertion,
- },
- logger,
- configurationUtilities
- );
- accessToken = `${tokenResult.tokenType} ${tokenResult.accessToken}`;
-
- // try to update connector_token SO
- try {
- await connectorTokenClient.updateOrReplace({
- connectorId,
- token: connectorToken,
- newToken: accessToken,
- expiresInSec: tokenResult.expiresIn,
- deleteExisting: hasErrors,
- });
- } catch (err) {
- logger.warn(
- `Not able to update ServiceNow connector token for connectorId: ${connectorId} due to error: ${err.message}`
- );
- }
- } else {
- // use existing valid token
- accessToken = connectorToken.token;
- }
- return accessToken;
-};
diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts
index d89a3c96b01b9..3f3895ec5b69f 100644
--- a/x-pack/plugins/actions/server/plugin.test.ts
+++ b/x-pack/plugins/actions/server/plugin.test.ts
@@ -109,7 +109,7 @@ describe('Actions Plugin', () => {
httpServerMock.createKibanaRequest(),
httpServerMock.createResponseFactory()
)) as unknown as ActionsApiRequestHandlerContext;
- actionsContextHandler!.getActionsClient();
+ expect(actionsContextHandler!.getActionsClient()).toBeDefined();
});
it('should throw error when ESO plugin is missing encryption key', async () => {
diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts
index 1fad2a6189693..c097b94a85950 100644
--- a/x-pack/plugins/actions/server/plugin.ts
+++ b/x-pack/plugins/actions/server/plugin.ts
@@ -311,12 +311,13 @@ export class ActionsPlugin implements Plugin(),
- this.licenseState,
+ defineRoutes({
+ router: core.http.createRouter(),
+ licenseState: this.licenseState,
+ logger: this.logger,
actionsConfigUtils,
- this.usageCounter
- );
+ usageCounter: this.usageCounter,
+ });
// Cleanup failed execution task definition
if (this.actionsConfig.cleanupFailedExecutionsTask.enabled) {
diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts
new file mode 100644
index 0000000000000..888e87dbdf1f4
--- /dev/null
+++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.test.ts
@@ -0,0 +1,227 @@
+/*
+ * 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 { getOAuthAccessToken } from './get_oauth_access_token';
+import { Logger } from '@kbn/core/server';
+import { httpServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
+import { licenseStateMock } from '../lib/license_state.mock';
+import { mockHandlerArguments } from './legacy/_mock_handler_arguments';
+import { verifyAccessAndContext } from './verify_access_and_context';
+import { actionsConfigMock } from '../actions_config.mock';
+import { actionsClientMock } from '../actions_client.mock';
+
+jest.mock('./verify_access_and_context', () => ({
+ verifyAccessAndContext: jest.fn(),
+}));
+
+const logger = loggingSystemMock.create().get() as jest.Mocked;
+const configurationUtilities = actionsConfigMock.create();
+
+beforeEach(() => {
+ jest.resetAllMocks();
+ (verifyAccessAndContext as jest.Mock).mockImplementation((license, handler) => handler);
+});
+
+describe('getOAuthAccessToken', () => {
+ it('returns jwt access token for given jwt oauth config', async () => {
+ const licenseState = licenseStateMock.create();
+ const router = httpServiceMock.createRouter();
+
+ getOAuthAccessToken(router, licenseState, logger, configurationUtilities);
+
+ const [config, handler] = router.post.mock.calls[0];
+
+ expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`);
+
+ const actionsClient = actionsClientMock.create();
+ actionsClient.getOAuthAccessToken.mockResolvedValueOnce({
+ accessToken: 'Bearer jwttokentokentoken',
+ });
+
+ const requestBody = {
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ };
+
+ const [context, req, res] = mockHandlerArguments(
+ { actionsClient },
+ {
+ body: requestBody,
+ },
+ ['ok']
+ );
+
+ expect(await handler(context, req, res)).toMatchInlineSnapshot(`
+ Object {
+ "body": Object {
+ "accessToken": "Bearer jwttokentokentoken",
+ },
+ }
+ `);
+
+ expect(actionsClient.getOAuthAccessToken).toHaveBeenCalledTimes(1);
+ expect(actionsClient.getOAuthAccessToken.mock.calls[0]).toEqual([
+ requestBody,
+ logger,
+ configurationUtilities,
+ ]);
+
+ expect(res.ok).toHaveBeenCalledWith({
+ body: {
+ accessToken: 'Bearer jwttokentokentoken',
+ },
+ });
+ });
+
+ it('returns client credentials access token for given client credentials oauth config', async () => {
+ const licenseState = licenseStateMock.create();
+ const router = httpServiceMock.createRouter();
+
+ getOAuthAccessToken(router, licenseState, logger, configurationUtilities);
+
+ const [config, handler] = router.post.mock.calls[0];
+
+ expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`);
+
+ const actionsClient = actionsClientMock.create();
+ actionsClient.getOAuthAccessToken.mockResolvedValueOnce({
+ accessToken: 'Bearer clienttokentokentoken',
+ });
+
+ const requestBody = {
+ type: 'client',
+ options: {
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ scope: 'https://graph.microsoft.com/.default',
+ config: {
+ clientId: 'abc',
+ tenantId: 'def',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ },
+ },
+ };
+
+ const [context, req, res] = mockHandlerArguments(
+ { actionsClient },
+ {
+ body: requestBody,
+ },
+ ['ok']
+ );
+
+ expect(await handler(context, req, res)).toMatchInlineSnapshot(`
+ Object {
+ "body": Object {
+ "accessToken": "Bearer clienttokentokentoken",
+ },
+ }
+ `);
+
+ expect(actionsClient.getOAuthAccessToken).toHaveBeenCalledTimes(1);
+ expect(actionsClient.getOAuthAccessToken.mock.calls[0]).toEqual([
+ requestBody,
+ logger,
+ configurationUtilities,
+ ]);
+
+ expect(res.ok).toHaveBeenCalledWith({
+ body: {
+ accessToken: 'Bearer clienttokentokentoken',
+ },
+ });
+ });
+
+ it('ensures the license allows getting servicenow access token', async () => {
+ const licenseState = licenseStateMock.create();
+ const router = httpServiceMock.createRouter();
+
+ getOAuthAccessToken(router, licenseState, logger, configurationUtilities);
+
+ const [config, handler] = router.post.mock.calls[0];
+
+ expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`);
+
+ const [context, req, res] = mockHandlerArguments(
+ {},
+ {
+ body: {
+ type: 'jwt',
+ options: {
+ tokenUrl: 'https://testurl.service-now.com/oauth_token.do',
+ config: {
+ clientId: 'abc',
+ jwtKeyId: 'def',
+ userIdentifierValue: 'userA',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ privateKey: 'xyz',
+ },
+ },
+ },
+ },
+ ['ok']
+ );
+
+ await handler(context, req, res);
+
+ expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
+ });
+
+ it('ensures the license check prevents getting service now access token', async () => {
+ const licenseState = licenseStateMock.create();
+ const router = httpServiceMock.createRouter();
+
+ (verifyAccessAndContext as jest.Mock).mockImplementation(() => async () => {
+ throw new Error('OMG');
+ });
+
+ getOAuthAccessToken(router, licenseState, logger, configurationUtilities);
+
+ const [config, handler] = router.post.mock.calls[0];
+
+ expect(config.path).toMatchInlineSnapshot(`"/internal/actions/connector/_oauth_access_token"`);
+
+ const [context, req, res] = mockHandlerArguments(
+ {},
+ {
+ body: {
+ type: 'client',
+ options: {
+ tokenUrl: 'https://login.microsoftonline.com/98765/oauth2/v2.0/token',
+ scope: 'https://graph.microsoft.com/.default',
+ config: {
+ clientId: 'abc',
+ tenantId: 'def',
+ },
+ secrets: {
+ clientSecret: 'iamasecret',
+ },
+ },
+ },
+ },
+ ['ok']
+ );
+
+ expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
+
+ expect(verifyAccessAndContext).toHaveBeenCalledWith(licenseState, expect.any(Function));
+ });
+});
diff --git a/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts b/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts
new file mode 100644
index 0000000000000..e1b612d321bcd
--- /dev/null
+++ b/x-pack/plugins/actions/server/routes/get_oauth_access_token.ts
@@ -0,0 +1,81 @@
+/*
+ * 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, TypeOf } from '@kbn/config-schema';
+import { IRouter, Logger } from '@kbn/core/server';
+import { ILicenseState } from '../lib';
+import { INTERNAL_BASE_ACTION_API_PATH } from '../../common';
+import { ActionsRequestHandlerContext } from '../types';
+import { verifyAccessAndContext } from './verify_access_and_context';
+import { ActionsConfigurationUtilities } from '../actions_config';
+
+const oauthJwtBodySchema = schema.object({
+ tokenUrl: schema.string(),
+ config: schema.object({
+ clientId: schema.string(),
+ jwtKeyId: schema.string(),
+ userIdentifierValue: schema.string(),
+ }),
+ secrets: schema.object({
+ clientSecret: schema.string(),
+ privateKey: schema.string(),
+ privateKeyPassword: schema.maybe(schema.string()),
+ }),
+});
+
+export type OAuthJwtParams = TypeOf;
+
+const oauthClientCredentialsBodySchema = schema.object({
+ tokenUrl: schema.string(),
+ scope: schema.string(),
+ config: schema.object({
+ clientId: schema.string(),
+ tenantId: schema.string(),
+ }),
+ secrets: schema.object({
+ clientSecret: schema.string(),
+ }),
+});
+
+export type OAuthClientCredentialsParams = TypeOf;
+
+const bodySchema = schema.object({
+ type: schema.oneOf([schema.literal('jwt'), schema.literal('client')]),
+ options: schema.conditional(
+ schema.siblingRef('type'),
+ schema.literal('jwt'),
+ oauthJwtBodySchema,
+ oauthClientCredentialsBodySchema
+ ),
+});
+
+export type OAuthParams = TypeOf;
+
+export const getOAuthAccessToken = (
+ router: IRouter,
+ licenseState: ILicenseState,
+ logger: Logger,
+ configurationUtilities: ActionsConfigurationUtilities
+) => {
+ router.post(
+ {
+ path: `${INTERNAL_BASE_ACTION_API_PATH}/connector/_oauth_access_token`,
+ validate: {
+ body: bodySchema,
+ },
+ },
+ router.handleLegacyErrors(
+ verifyAccessAndContext(licenseState, async function (context, req, res) {
+ const actionsClient = (await context.actions).getActionsClient();
+
+ return res.ok({
+ body: await actionsClient.getOAuthAccessToken(req.body, logger, configurationUtilities),
+ });
+ })
+ )
+ );
+};
diff --git a/x-pack/plugins/actions/server/routes/index.ts b/x-pack/plugins/actions/server/routes/index.ts
index ab90141ae1c80..2822aa3668900 100644
--- a/x-pack/plugins/actions/server/routes/index.ts
+++ b/x-pack/plugins/actions/server/routes/index.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { IRouter } from '@kbn/core/server';
+import { IRouter, Logger } from '@kbn/core/server';
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
import { ILicenseState } from '../lib';
import { ActionsRequestHandlerContext } from '../types';
@@ -17,15 +17,21 @@ import { getAllActionRoute } from './get_all';
import { connectorTypesRoute } from './connector_types';
import { updateActionRoute } from './update';
import { getWellKnownEmailServiceRoute } from './get_well_known_email_service';
+import { getOAuthAccessToken } from './get_oauth_access_token';
import { defineLegacyRoutes } from './legacy';
import { ActionsConfigurationUtilities } from '../actions_config';
-export function defineRoutes(
- router: IRouter,
- licenseState: ILicenseState,
- actionsConfigUtils: ActionsConfigurationUtilities,
- usageCounter?: UsageCounter
-) {
+export interface RouteOptions {
+ router: IRouter;
+ licenseState: ILicenseState;
+ logger: Logger;
+ actionsConfigUtils: ActionsConfigurationUtilities;
+ usageCounter?: UsageCounter;
+}
+
+export function defineRoutes(opts: RouteOptions) {
+ const { router, licenseState, logger, actionsConfigUtils, usageCounter } = opts;
+
defineLegacyRoutes(router, licenseState, usageCounter);
createActionRoute(router, licenseState);
@@ -36,5 +42,6 @@ export function defineRoutes(
connectorTypesRoute(router, licenseState);
executeActionRoute(router, licenseState);
+ getOAuthAccessToken(router, licenseState, logger, actionsConfigUtils);
getWellKnownEmailServiceRoute(router, licenseState);
}
diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts
index c8f282bf695d7..4509a004c6e58 100644
--- a/x-pack/plugins/alerting/common/rule.ts
+++ b/x-pack/plugins/alerting/common/rule.ts
@@ -75,6 +75,7 @@ export interface RuleAggregations {
ruleEnabledStatus: { enabled: number; disabled: number };
ruleMutedStatus: { muted: number; unmuted: number };
ruleSnoozedStatus: { snoozed: number };
+ ruleTags: string[];
}
export interface MappedParamsProperties {
diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts
index 7123f1bf4ad6c..8c24b457df565 100644
--- a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts
+++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts
@@ -60,6 +60,7 @@ describe('aggregateRulesRoute', () => {
ruleSnoozedStatus: {
snoozed: 4,
},
+ ruleTags: ['a', 'b', 'c'],
};
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);
@@ -94,6 +95,11 @@ describe('aggregateRulesRoute', () => {
"rule_snoozed_status": Object {
"snoozed": 4,
},
+ "rule_tags": Array [
+ "a",
+ "b",
+ "c",
+ ],
},
}
`);
@@ -129,6 +135,7 @@ describe('aggregateRulesRoute', () => {
rule_snoozed_status: {
snoozed: 4,
},
+ rule_tags: ['a', 'b', 'c'],
},
});
});
diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts
index 312def72dd65e..c48c74fc28754 100644
--- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts
+++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts
@@ -50,6 +50,7 @@ const rewriteBodyRes: RewriteResponseCase = ({
ruleEnabledStatus,
ruleMutedStatus,
ruleSnoozedStatus,
+ ruleTags,
...rest
}) => ({
...rest,
@@ -57,6 +58,7 @@ const rewriteBodyRes: RewriteResponseCase = ({
rule_enabled_status: ruleEnabledStatus,
rule_muted_status: ruleMutedStatus,
rule_snoozed_status: ruleSnoozedStatus,
+ rule_tags: ruleTags,
});
export const aggregateRulesRoute = (
diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
index 00f67437ae4f2..e229b15fcd1cd 100644
--- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts
+++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts
@@ -133,6 +133,12 @@ export interface RuleAggregation {
doc_count: number;
}>;
};
+ tags: {
+ buckets: Array<{
+ key: string;
+ doc_count: number;
+ }>;
+ };
}
export interface ConstructorOptions {
@@ -200,6 +206,7 @@ export interface AggregateResult {
ruleEnabledStatus?: { enabled: number; disabled: number };
ruleMutedStatus?: { muted: number; unmuted: number };
ruleSnoozedStatus?: { snoozed: number };
+ ruleTags?: string[];
}
export interface FindResult {
@@ -921,6 +928,9 @@ export class RulesClient {
muted: {
terms: { field: 'alert.attributes.muteAll' },
},
+ tags: {
+ terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } },
+ },
snoozed: {
date_range: {
field: 'alert.attributes.snoozeEndTime',
@@ -990,6 +1000,9 @@ export class RulesClient {
snoozed: snoozedBuckets.reduce((acc, bucket) => acc + bucket.doc_count, 0),
};
+ const tagsBuckets = resp.aggregations.tags?.buckets || [];
+ ret.ruleTags = tagsBuckets.map((bucket) => bucket.key);
+
return ret;
}
diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts
index b74059e4be3d6..1a3d203162bd6 100644
--- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts
+++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts
@@ -112,6 +112,22 @@ describe('aggregate()', () => {
},
],
},
+ tags: {
+ buckets: [
+ {
+ key: 'a',
+ doc_count: 10,
+ },
+ {
+ key: 'b',
+ doc_count: 20,
+ },
+ {
+ key: 'c',
+ doc_count: 30,
+ },
+ ],
+ },
},
});
@@ -160,6 +176,11 @@ describe('aggregate()', () => {
"ruleSnoozedStatus": Object {
"snoozed": 2,
},
+ "ruleTags": Array [
+ "a",
+ "b",
+ "c",
+ ],
}
`);
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
@@ -187,6 +208,9 @@ describe('aggregate()', () => {
ranges: [{ from: 'now' }],
},
},
+ tags: {
+ terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } },
+ },
},
},
]);
@@ -221,6 +245,9 @@ describe('aggregate()', () => {
ranges: [{ from: 'now' }],
},
},
+ tags: {
+ terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } },
+ },
},
},
]);
diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx
index ac2729564b387..50a3c69f2073e 100644
--- a/x-pack/plugins/cases/public/components/create/form.tsx
+++ b/x-pack/plugins/cases/public/components/create/form.tsx
@@ -35,6 +35,7 @@ import { CreateCaseOwnerSelector } from './owner_selector';
import { useCasesContext } from '../cases_context/use_cases_context';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import { CaseAttachments } from '../../types';
+import { Severity } from './severity';
interface ContainerProps {
big?: boolean;
@@ -88,6 +89,9 @@ export const CreateCaseFormFields: React.FC = React.m
+
+
+
{canShowCaseSolutionSelection && (
= React.m
+
>
),
}),
diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx
index 634f518ae5ebd..bfa4f391458da 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx
@@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme';
import { act, RenderResult, waitFor, within } from '@testing-library/react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
-import { CommentType, ConnectorTypes } from '../../../common/api';
+import { CaseSeverity, CommentType, ConnectorTypes } from '../../../common/api';
import { useKibana } from '../../common/lib/kibana';
import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../../common/mock';
import { usePostCase } from '../../containers/use_post_case';
@@ -182,6 +182,7 @@ describe('Create case', () => {
);
expect(renderResult.getByTestId('caseTitle')).toBeTruthy();
+ expect(renderResult.getByTestId('caseSeverity')).toBeTruthy();
expect(renderResult.getByTestId('caseDescription')).toBeTruthy();
expect(renderResult.getByTestId('caseTags')).toBeTruthy();
expect(renderResult.getByTestId('caseConnectors')).toBeTruthy();
@@ -208,6 +209,34 @@ describe('Create case', () => {
});
});
+ it('should post a case on submit click with the selected severity', async () => {
+ useConnectorsMock.mockReturnValue({
+ ...sampleConnectorData,
+ connectors: connectorsMock,
+ });
+
+ const renderResult = mockedContext.render(
+
+
+
+
+ );
+
+ await fillFormReactTestingLib(renderResult);
+
+ userEvent.click(renderResult.getByTestId('case-severity-selection'));
+ expect(renderResult.getByTestId('case-severity-selection-high')).toBeTruthy();
+ userEvent.click(renderResult.getByTestId('case-severity-selection-high'));
+
+ userEvent.click(renderResult.getByTestId('create-case-submit'));
+ await waitFor(() => {
+ expect(postCase).toBeCalledWith({
+ ...sampleData,
+ severity: CaseSeverity.HIGH,
+ });
+ });
+ });
+
it('does not submits the title when the length is longer than 64 characters', async () => {
const longTitle =
'This is a title that should not be saved as it is longer than 64 characters.';
@@ -285,6 +314,18 @@ describe('Create case', () => {
);
});
+ it('should select LOW as the default severity', async () => {
+ const renderResult = mockedContext.render(
+
+
+
+
+ );
+ expect(renderResult.getByTestId('caseSeverity')).toBeTruthy();
+ // there should be 2 low elements. one for the options popover and one for the displayed one.
+ expect(renderResult.getAllByTestId('case-severity-selection-low').length).toBe(2);
+ });
+
it('should select the default connector set in the configuration', async () => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx
index 4385053a8c8c0..a65e9f5960e9d 100644
--- a/x-pack/plugins/cases/public/components/create/form_context.tsx
+++ b/x-pack/plugins/cases/public/components/create/form_context.tsx
@@ -14,7 +14,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service'
import { useConnectors } from '../../containers/configure/use_connectors';
import { Case } from '../../containers/types';
-import { NONE_CONNECTOR_ID } from '../../../common/api';
+import { CaseSeverity, NONE_CONNECTOR_ID } from '../../../common/api';
import {
UseCreateAttachments,
useCreateAttachments,
@@ -28,6 +28,7 @@ const initialCaseValue: FormProps = {
description: '',
tags: [],
title: '',
+ severity: CaseSeverity.LOW,
connectorId: NONE_CONNECTOR_ID,
fields: null,
syncAlerts: true,
diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts
index 8ab515c79f67e..38d57bf24781e 100644
--- a/x-pack/plugins/cases/public/components/create/mock.ts
+++ b/x-pack/plugins/cases/public/components/create/mock.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { CasePostRequest, ConnectorTypes } from '../../../common/api';
+import { CasePostRequest, CaseSeverity, ConnectorTypes } from '../../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { choices } from '../connectors/mock';
@@ -13,6 +13,7 @@ export const sampleTags = ['coke', 'pepsi'];
export const sampleData: CasePostRequest = {
description: 'what a great description',
tags: sampleTags,
+ severity: CaseSeverity.LOW,
title: 'what a cool title',
connector: {
fields: null,
diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx
index b7c363b263998..d72b1cc523f0d 100644
--- a/x-pack/plugins/cases/public/components/create/schema.tsx
+++ b/x-pack/plugins/cases/public/components/create/schema.tsx
@@ -17,6 +17,7 @@ import {
import * as i18n from './translations';
import { OptionalFieldLabel } from './optional_field_label';
+import { SEVERITY_TITLE } from '../severity/translations';
const { emptyField, maxLengthField } = fieldValidators;
export const schemaTags = {
@@ -83,6 +84,9 @@ export const schema: FormSchema = {
],
},
tags: schemaTags,
+ severity: {
+ label: SEVERITY_TITLE,
+ },
connectorId: {
type: FIELD_TYPES.SUPER_SELECT,
label: i18n.CONNECTORS,
diff --git a/x-pack/plugins/cases/public/components/create/severity.test.tsx b/x-pack/plugins/cases/public/components/create/severity.test.tsx
new file mode 100644
index 0000000000000..d2434a37a4392
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/create/severity.test.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { CaseSeverity } from '../../../common/api';
+import React from 'react';
+import { AppMockRenderer, createAppMockRenderer } from '../../common/mock';
+import { Form, FormHook, useForm } from '../../common/shared_imports';
+import { Severity } from './severity';
+import { FormProps, schema } from './schema';
+import userEvent from '@testing-library/user-event';
+import { waitFor } from '@testing-library/dom';
+
+let globalForm: FormHook;
+const MockHookWrapperComponent: React.FC = ({ children }) => {
+ const { form } = useForm({
+ defaultValue: { severity: CaseSeverity.LOW },
+ schema: {
+ severity: schema.severity,
+ },
+ });
+
+ globalForm = form;
+
+ return ;
+};
+describe('Severity form field', () => {
+ let appMockRender: AppMockRenderer;
+ beforeEach(() => {
+ appMockRender = createAppMockRenderer();
+ });
+ it('renders', () => {
+ const result = appMockRender.render(
+
+
+
+ );
+ expect(result.getByTestId('caseSeverity')).toBeTruthy();
+ expect(result.getByTestId('case-severity-selection')).not.toHaveAttribute('disabled');
+ });
+
+ // default to LOW in this test configuration
+ it('defaults to the correct value', () => {
+ const result = appMockRender.render(
+
+
+
+ );
+ expect(result.getByTestId('caseSeverity')).toBeTruthy();
+ // two items. one for the popover one for the selected field
+ expect(result.getAllByTestId('case-severity-selection-low').length).toBe(2);
+ });
+
+ it('selects the correct value when changed', async () => {
+ const result = appMockRender.render(
+
+
+
+ );
+ expect(result.getByTestId('caseSeverity')).toBeTruthy();
+ userEvent.click(result.getByTestId('case-severity-selection'));
+ userEvent.click(result.getByTestId('case-severity-selection-high'));
+ await waitFor(() => {
+ expect(globalForm.getFormData()).toEqual({ severity: 'high' });
+ });
+ });
+
+ it('disables when loading data', () => {
+ const result = appMockRender.render(
+
+
+
+ );
+ expect(result.getByTestId('case-severity-selection')).toHaveAttribute('disabled');
+ });
+});
diff --git a/x-pack/plugins/cases/public/components/create/severity.tsx b/x-pack/plugins/cases/public/components/create/severity.tsx
new file mode 100644
index 0000000000000..730eab5d77ac6
--- /dev/null
+++ b/x-pack/plugins/cases/public/components/create/severity.tsx
@@ -0,0 +1,50 @@
+/*
+ * 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 { EuiFormRow } from '@elastic/eui';
+import React, { memo } from 'react';
+import { CaseSeverity } from '../../../common/api';
+import { UseField, useFormContext, useFormData } from '../../common/shared_imports';
+import { SeveritySelector } from '../severity/selector';
+import { SEVERITY_TITLE } from '../severity/translations';
+
+interface Props {
+ isLoading: boolean;
+}
+
+const SeverityFieldFormComponent = ({ isLoading }: { isLoading: boolean }) => {
+ const { setFieldValue } = useFormContext();
+ const [{ severity }] = useFormData({ watch: ['severity'] });
+ const onSeverityChange = (newSeverity: CaseSeverity) => {
+ setFieldValue('severity', newSeverity);
+ };
+ return (
+
+
+
+ );
+};
+SeverityFieldFormComponent.displayName = 'SeverityFieldForm';
+
+const SeverityComponent: React.FC = ({ isLoading }) => (
+
+);
+
+SeverityComponent.displayName = 'SeverityComponent';
+
+export const Severity = memo(SeverityComponent);
diff --git a/x-pack/plugins/cloud/public/plugin.test.ts b/x-pack/plugins/cloud/public/plugin.test.ts
index cfd0d45667417..36be9e590f216 100644
--- a/x-pack/plugins/cloud/public/plugin.test.ts
+++ b/x-pack/plugins/cloud/public/plugin.test.ts
@@ -11,6 +11,7 @@ import { homePluginMock } from '@kbn/home-plugin/public/mocks';
import { securityMock } from '@kbn/security-plugin/public/mocks';
import { CloudPlugin, CloudConfigType } from './plugin';
import { firstValueFrom } from 'rxjs';
+import { Sha256 } from '@kbn/core/public/utils';
describe('Cloud Plugin', () => {
describe('#setup', () => {
@@ -74,6 +75,9 @@ describe('Cloud Plugin', () => {
});
describe('setupTelemetryContext', () => {
+ const username = '1234';
+ const expectedHashedPlainUsername = new Sha256().update(username, 'utf8').digest('hex');
+
beforeEach(() => {
jest.clearAllMocks();
});
@@ -121,9 +125,7 @@ describe('Cloud Plugin', () => {
test('register the context provider for the cloud user with hashed user ID when security is available', async () => {
const { coreSetup } = await setupPlugin({
config: { id: 'cloudId' },
- currentUserProps: {
- username: '1234',
- },
+ currentUserProps: { username },
});
expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
@@ -140,9 +142,7 @@ describe('Cloud Plugin', () => {
it('user hash includes cloud id', async () => {
const { coreSetup: coreSetup1 } = await setupPlugin({
config: { id: 'esOrg1' },
- currentUserProps: {
- username: '1234',
- },
+ currentUserProps: { username },
});
const [{ context$: context1$ }] =
@@ -151,12 +151,11 @@ describe('Cloud Plugin', () => {
)!;
const hashId1 = await firstValueFrom(context1$);
+ expect(hashId1).not.toEqual(expectedHashedPlainUsername);
const { coreSetup: coreSetup2 } = await setupPlugin({
config: { full_story: { enabled: true, org_id: 'foo' }, id: 'esOrg2' },
- currentUserProps: {
- username: '1234',
- },
+ currentUserProps: { username },
});
const [{ context$: context2$ }] =
@@ -165,15 +164,17 @@ describe('Cloud Plugin', () => {
)!;
const hashId2 = await firstValueFrom(context2$);
+ expect(hashId2).not.toEqual(expectedHashedPlainUsername);
expect(hashId1).not.toEqual(hashId2);
});
- test('user hash does not include cloudId when not provided', async () => {
+ test('user hash does not include cloudId when authenticated via Cloud SAML', async () => {
const { coreSetup } = await setupPlugin({
- config: {},
+ config: { id: 'cloudDeploymentId' },
currentUserProps: {
- username: '1234',
+ username,
+ authentication_realm: { type: 'saml', name: 'cloud-saml-kibana' },
},
});
@@ -184,7 +185,24 @@ describe('Cloud Plugin', () => {
)!;
await expect(firstValueFrom(context$)).resolves.toEqual({
- userId: '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4',
+ userId: expectedHashedPlainUsername,
+ });
+ });
+
+ test('user hash does not include cloudId when not provided', async () => {
+ const { coreSetup } = await setupPlugin({
+ config: {},
+ currentUserProps: { username },
+ });
+
+ expect(coreSetup.analytics.registerContextProvider).toHaveBeenCalled();
+
+ const [{ context$ }] = coreSetup.analytics.registerContextProvider.mock.calls.find(
+ ([{ name }]) => name === 'cloud_user_id'
+ )!;
+
+ await expect(firstValueFrom(context$)).resolves.toEqual({
+ userId: expectedHashedPlainUsername,
});
});
diff --git a/x-pack/plugins/cloud/public/plugin.tsx b/x-pack/plugins/cloud/public/plugin.tsx
index db6b2305495bf..1bccf219225dc 100644
--- a/x-pack/plugins/cloud/public/plugin.tsx
+++ b/x-pack/plugins/cloud/public/plugin.tsx
@@ -261,10 +261,21 @@ export class CloudPlugin implements Plugin {
analytics.registerContextProvider({
name: 'cloud_user_id',
context$: from(security.authc.getCurrentUser()).pipe(
- map((user) => user.username),
+ map((user) => {
+ if (
+ getIsCloudEnabled(cloudId) &&
+ user.authentication_realm?.type === 'saml' &&
+ user.authentication_realm?.name === 'cloud-saml-kibana'
+ ) {
+ // If authenticated via Cloud SAML, use the SAML username as the user ID
+ return user.username;
+ }
+
+ return cloudId ? `${cloudId}:${user.username}` : user.username;
+ }),
// Join the cloud org id and the user to create a truly unique user id.
// The hashing here is to keep it at clear as possible in our source code that we do not send literal user IDs
- map((userId) => ({ userId: sha256(cloudId ? `${cloudId}:${userId}` : `${userId}`) })),
+ map((userId) => ({ userId: sha256(userId) })),
catchError(() => of({ userId: undefined }))
),
schema: {
diff --git a/x-pack/plugins/cloud_security_posture/common/constants.ts b/x-pack/plugins/cloud_security_posture/common/constants.ts
index b2edd268c8485..30e9651b6e739 100644
--- a/x-pack/plugins/cloud_security_posture/common/constants.ts
+++ b/x-pack/plugins/cloud_security_posture/common/constants.ts
@@ -31,7 +31,7 @@ export const INTERNAL_FEATURE_FLAGS = {
showBenchmarks: true,
showManageRulesMock: false,
showRisksMock: false,
- showFindingsGroupBy: false,
+ showFindingsGroupBy: true,
} as const;
export const cspRuleAssetSavedObjectType = 'csp_rule';
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts
index 574534290214a..5f093a19157f9 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/index.ts
@@ -5,6 +5,6 @@
* 2.0.
*/
-export { installPipelines, isTopLevelPipeline } from './install';
+export { prepareToInstallPipelines, isTopLevelPipeline } from './install';
export { deletePreviousPipelines, deletePipeline } from './remove';
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
index 49dae4d86b639..da035a44c9921 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/install.ts
@@ -6,13 +6,12 @@
*/
import type { TransportRequestOptions } from '@elastic/elasticsearch';
-import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server';
+import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { ElasticsearchAssetType } from '../../../../types';
import type { EsAssetReference, RegistryDataStream, InstallablePackage } from '../../../../types';
import { getAsset, getPathParts } from '../../archive';
import type { ArchiveEntry } from '../../archive';
-import { updateEsAssetReferences } from '../../packages/install';
import {
FLEET_FINAL_PIPELINE_CONTENT,
FLEET_FINAL_PIPELINE_ID,
@@ -36,23 +35,23 @@ export const isTopLevelPipeline = (path: string) => {
);
};
-export const installPipelines = async (
+export const prepareToInstallPipelines = (
installablePackage: InstallablePackage,
- paths: string[],
- esClient: ElasticsearchClient,
- savedObjectsClient: SavedObjectsClientContract,
- logger: Logger,
- esReferences: EsAssetReference[]
-) => {
+ paths: string[]
+): {
+ assetsToAdd: EsAssetReference[];
+ install: (esClient: ElasticsearchClient, logger: Logger) => Promise;
+} => {
// unlike other ES assets, pipeline names are versioned so after a template is updated
// it can be created pointing to the new template, without removing the old one and effecting data
// so do not remove the currently installed pipelines here
const dataStreams = installablePackage.data_streams;
- const { name: pkgName, version: pkgVersion } = installablePackage;
+ const { version: pkgVersion } = installablePackage;
const pipelinePaths = paths.filter((path) => isPipeline(path));
const topLevelPipelinePaths = paths.filter((path) => isTopLevelPipeline(path));
- if (!dataStreams?.length && topLevelPipelinePaths.length === 0) return [];
+ if (!dataStreams?.length && topLevelPipelinePaths.length === 0)
+ return { assetsToAdd: [], install: () => Promise.resolve() };
// get and save pipeline refs before installing pipelines
let pipelineRefs = dataStreams
@@ -85,41 +84,41 @@ export const installPipelines = async (
pipelineRefs = [...pipelineRefs, ...topLevelPipelineRefs];
- esReferences = await updateEsAssetReferences(savedObjectsClient, pkgName, esReferences, {
+ return {
assetsToAdd: pipelineRefs,
- });
-
- const pipelines = dataStreams
- ? dataStreams.reduce>>((acc, dataStream) => {
- if (dataStream.ingest_pipeline) {
- acc.push(
- installAllPipelines({
- dataStream,
- esClient,
- logger,
- paths: pipelinePaths,
- installablePackage,
- })
- );
- }
- return acc;
- }, [])
- : [];
-
- if (topLevelPipelinePaths) {
- pipelines.push(
- installAllPipelines({
- dataStream: undefined,
- esClient,
- logger,
- paths: topLevelPipelinePaths,
- installablePackage,
- })
- );
- }
+ install: async (esClient, logger) => {
+ const pipelines = dataStreams
+ ? dataStreams.reduce>>((acc, dataStream) => {
+ if (dataStream.ingest_pipeline) {
+ acc.push(
+ installAllPipelines({
+ dataStream,
+ esClient,
+ logger,
+ paths: pipelinePaths,
+ installablePackage,
+ })
+ );
+ }
+ return acc;
+ }, [])
+ : [];
+
+ if (topLevelPipelinePaths) {
+ pipelines.push(
+ installAllPipelines({
+ dataStream: undefined,
+ esClient,
+ logger,
+ paths: topLevelPipelinePaths,
+ installablePackage,
+ })
+ );
+ }
- await Promise.all(pipelines);
- return esReferences;
+ await Promise.all(pipelines);
+ },
+ };
};
export function rewriteIngestPipeline(
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts
index 998d0f9fb1ae5..3478da69bf721 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts
@@ -4,30 +4,19 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
-import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { loggerMock } from '@kbn/logging-mocks';
-
import { createAppContextStartContractMock } from '../../../../mocks';
import { appContextService } from '../../..';
import type { RegistryDataStream } from '../../../../types';
-import type { Field } from '../../fields/field';
-import { installTemplate } from './install';
+import { prepareTemplate } from './install';
-describe('EPM install', () => {
+describe('EPM index template install', () => {
beforeEach(async () => {
appContextService.start(createAppContextStartContractMock());
});
- it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => {
- const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- esClient.indices.getIndexTemplate.mockImplementation(() =>
- elasticsearchServiceMock.createSuccessTransportRequestPromise({ index_templates: [] })
- );
-
- const fields: Field[] = [];
+ it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix not set', async () => {
const dataStreamDatasetIsPrefixUnset = {
type: 'metrics',
dataset: 'package.dataset',
@@ -43,29 +32,14 @@ describe('EPM install', () => {
};
const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*';
const templatePriorityDatasetIsPrefixUnset = 200;
- await installTemplate({
- esClient,
- logger: loggerMock.create(),
- fields,
- dataStream: dataStreamDatasetIsPrefixUnset,
- packageVersion: pkg.version,
- packageName: pkg.name,
- });
-
- const sentTemplate = (
- esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest
- ).body;
-
- expect(sentTemplate).toBeDefined();
- expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset);
- expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]);
+ const {
+ indexTemplate: { indexTemplate },
+ } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixUnset });
+ expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixUnset);
+ expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]);
});
- it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => {
- const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- esClient.indices.getIndexTemplate.mockResponse({ index_templates: [] });
-
- const fields: Field[] = [];
+ it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix set to false', async () => {
const dataStreamDatasetIsPrefixFalse = {
type: 'metrics',
dataset: 'package.dataset',
@@ -82,29 +56,15 @@ describe('EPM install', () => {
};
const templateIndexPatternDatasetIsPrefixFalse = 'metrics-package.dataset-*';
const templatePriorityDatasetIsPrefixFalse = 200;
- await installTemplate({
- esClient,
- logger: loggerMock.create(),
- fields,
- dataStream: dataStreamDatasetIsPrefixFalse,
- packageVersion: pkg.version,
- packageName: pkg.name,
- });
-
- const sentTemplate = (
- esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest
- ).body;
+ const {
+ indexTemplate: { indexTemplate },
+ } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixFalse });
- expect(sentTemplate).toBeDefined();
- expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixFalse);
- expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]);
+ expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixFalse);
+ expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixFalse]);
});
- it('tests installPackage to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => {
- const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
- esClient.indices.getIndexTemplate.mockResponse({ index_templates: [] });
-
- const fields: Field[] = [];
+ it('tests prepareTemplate to use correct priority and index_patterns for data stream with dataset_is_prefix set to true', async () => {
const dataStreamDatasetIsPrefixTrue = {
type: 'metrics',
dataset: 'package.dataset',
@@ -121,71 +81,11 @@ describe('EPM install', () => {
};
const templateIndexPatternDatasetIsPrefixTrue = 'metrics-package.dataset.*-*';
const templatePriorityDatasetIsPrefixTrue = 150;
- await installTemplate({
- esClient,
- logger: loggerMock.create(),
- fields,
- dataStream: dataStreamDatasetIsPrefixTrue,
- packageVersion: pkg.version,
- packageName: pkg.name,
- });
-
- const sentTemplate = (
- esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest
- ).body;
-
- expect(sentTemplate).toBeDefined();
- expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixTrue);
- expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]);
- });
-
- it('tests installPackage remove the aliases property if the property existed', async () => {
- const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
-
- esClient.indices.getIndexTemplate.mockResponse({
- index_templates: [
- {
- name: 'metrics-package.dataset',
- // @ts-expect-error not full interface
- index_template: {
- index_patterns: ['metrics-package.dataset-*'],
- template: { aliases: {} },
- },
- },
- ],
- });
-
- const fields: Field[] = [];
- const dataStreamDatasetIsPrefixUnset = {
- type: 'metrics',
- dataset: 'package.dataset',
- title: 'test data stream',
- release: 'experimental',
- package: 'package',
- path: 'path',
- ingest_pipeline: 'default',
- } as RegistryDataStream;
- const pkg = {
- name: 'package',
- version: '0.0.1',
- };
- const templateIndexPatternDatasetIsPrefixUnset = 'metrics-package.dataset-*';
- const templatePriorityDatasetIsPrefixUnset = 200;
- await installTemplate({
- esClient,
- logger: loggerMock.create(),
- fields,
- dataStream: dataStreamDatasetIsPrefixUnset,
- packageVersion: pkg.version,
- packageName: pkg.name,
- });
+ const {
+ indexTemplate: { indexTemplate },
+ } = prepareTemplate({ pkg, dataStream: dataStreamDatasetIsPrefixTrue });
- const sentTemplate = (
- esClient.indices.putIndexTemplate.mock.calls[0][0] as estypes.IndicesPutIndexTemplateRequest
- ).body;
- expect(sentTemplate).toBeDefined();
- expect(sentTemplate?.template?.aliases).not.toBeDefined();
- expect(sentTemplate?.priority).toBe(templatePriorityDatasetIsPrefixUnset);
- expect(sentTemplate?.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixUnset]);
+ expect(indexTemplate.priority).toBe(templatePriorityDatasetIsPrefixTrue);
+ expect(indexTemplate.index_patterns).toEqual([templateIndexPatternDatasetIsPrefixTrue]);
});
});
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
index 2d2e5b2ffea2a..df6d9d84a08c5 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts
@@ -7,7 +7,7 @@
import { merge } from 'lodash';
import Boom from '@hapi/boom';
-import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server';
+import type { ElasticsearchClient, Logger } from '@kbn/core/server';
import { ElasticsearchAssetType } from '../../../../types';
import type {
@@ -20,13 +20,12 @@ import type {
TemplateMapEntry,
TemplateMap,
EsAssetReference,
+ PackageInfo,
} from '../../../../types';
import { loadFieldsFromYaml, processFields } from '../../fields/field';
-import type { Field } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
import { getAsset, getPathParts } from '../../archive';
-import { updateEsAssetReferences } from '../../packages/install';
import {
FLEET_COMPONENT_TEMPLATES,
PACKAGE_TEMPLATE_SUFFIX,
@@ -47,65 +46,55 @@ import { buildDefaultSettings } from './default_settings';
const FLEET_COMPONENT_TEMPLATE_NAMES = FLEET_COMPONENT_TEMPLATES.map((tmpl) => tmpl.name);
-export const installTemplates = async (
+export const prepareToInstallTemplates = (
installablePackage: InstallablePackage,
- esClient: ElasticsearchClient,
- logger: Logger,
paths: string[],
- savedObjectsClient: SavedObjectsClientContract,
esReferences: EsAssetReference[]
-): Promise<{
- installedTemplates: IndexTemplateEntry[];
- installedEsReferences: EsAssetReference[];
-}> => {
- // install any pre-built index template assets,
- // atm, this is only the base package's global index templates
- // Install component templates first, as they are used by the index templates
- await installPreBuiltComponentTemplates(paths, esClient, logger);
- await installPreBuiltTemplates(paths, esClient, logger);
-
+): {
+ assetsToAdd: EsAssetReference[];
+ assetsToRemove: EsAssetReference[];
+ install: (esClient: ElasticsearchClient, logger: Logger) => Promise;
+} => {
// remove package installation's references to index templates
- esReferences = await updateEsAssetReferences(
- savedObjectsClient,
- installablePackage.name,
- esReferences,
- {
- assetsToRemove: esReferences.filter(
- ({ type }) =>
- type === ElasticsearchAssetType.indexTemplate ||
- type === ElasticsearchAssetType.componentTemplate
- ),
- }
+ const assetsToRemove = esReferences.filter(
+ ({ type }) =>
+ type === ElasticsearchAssetType.indexTemplate ||
+ type === ElasticsearchAssetType.componentTemplate
);
// build templates per data stream from yml files
const dataStreams = installablePackage.data_streams;
- if (!dataStreams) return { installedTemplates: [], installedEsReferences: esReferences };
-
- const installedTemplatesNested = await Promise.all(
- dataStreams.map((dataStream) =>
- installTemplateForDataStream({
- pkg: installablePackage,
- esClient,
- logger,
- dataStream,
- })
- )
- );
- const installedTemplates = installedTemplatesNested.flat();
+ if (!dataStreams) return { assetsToAdd: [], assetsToRemove, install: () => Promise.resolve([]) };
- // get template refs to save
- const installedIndexTemplateRefs = getAllTemplateRefs(installedTemplates);
-
- // add package installation's references to index templates
- esReferences = await updateEsAssetReferences(
- savedObjectsClient,
- installablePackage.name,
- esReferences,
- { assetsToAdd: installedIndexTemplateRefs }
+ const templates = dataStreams.map((dataStream) =>
+ prepareTemplate({ pkg: installablePackage, dataStream })
);
+ const assetsToAdd = getAllTemplateRefs(templates.map((template) => template.indexTemplate));
+
+ return {
+ assetsToAdd,
+ assetsToRemove,
+ install: async (esClient, logger) => {
+ // install any pre-built index template assets,
+ // atm, this is only the base package's global index templates
+ // Install component templates first, as they are used by the index templates
+ await installPreBuiltComponentTemplates(paths, esClient, logger);
+ await installPreBuiltTemplates(paths, esClient, logger);
+
+ await Promise.all(
+ templates.map((template) =>
+ installComponentAndIndexTemplateForDataStream({
+ esClient,
+ logger,
+ componentTemplates: template.componentTemplates,
+ indexTemplate: template.indexTemplate,
+ })
+ )
+ );
- return { installedTemplates, installedEsReferences: esReferences };
+ return templates.map((template) => template.indexTemplate);
+ },
+ };
};
const installPreBuiltTemplates = async (
@@ -187,31 +176,24 @@ const isComponentTemplate = (path: string) => {
};
/**
- * installTemplateForDataStream installs one template for each data stream
+ * installComponentAndIndexTemplateForDataStream installs one template for each data stream
*
* The template is currently loaded with the pkgkey-package-data_stream
*/
-export async function installTemplateForDataStream({
- pkg,
+export async function installComponentAndIndexTemplateForDataStream({
esClient,
logger,
- dataStream,
+ componentTemplates,
+ indexTemplate,
}: {
- pkg: InstallablePackage;
esClient: ElasticsearchClient;
logger: Logger;
- dataStream: RegistryDataStream;
-}): Promise {
- const fields = await loadFieldsFromYaml(pkg, dataStream.path);
- return installTemplate({
- esClient,
- logger,
- fields,
- dataStream,
- packageVersion: pkg.version,
- packageName: pkg.name,
- });
+ componentTemplates: TemplateMap;
+ indexTemplate: IndexTemplateEntry;
+}) {
+ await installDataStreamComponentTemplates({ esClient, logger, componentTemplates });
+ await installTemplate({ esClient, logger, template: indexTemplate });
}
function putComponentTemplate(
@@ -291,35 +273,18 @@ function buildComponentTemplates(params: {
return templatesMap;
}
-async function installDataStreamComponentTemplates(params: {
- mappings: IndexTemplateMappings;
- templateName: string;
- registryElasticsearch: RegistryElasticsearch | undefined;
+async function installDataStreamComponentTemplates({
+ esClient,
+ logger,
+ componentTemplates,
+}: {
esClient: ElasticsearchClient;
logger: Logger;
- packageName: string;
- defaultSettings: IndexTemplate['template']['settings'];
+ componentTemplates: TemplateMap;
}) {
- const {
- templateName,
- registryElasticsearch,
- esClient,
- packageName,
- defaultSettings,
- logger,
- mappings,
- } = params;
- const componentTemplates = buildComponentTemplates({
- mappings,
- templateName,
- registryElasticsearch,
- packageName,
- defaultSettings,
- });
- const templateEntries = Object.entries(componentTemplates);
// TODO: Check return values for errors
await Promise.all(
- templateEntries.map(async ([name, body]) => {
+ Object.entries(componentTemplates).map(async ([name, body]) => {
if (isUserSettingsTemplate(name)) {
try {
// Attempt to create custom component templates, ignore if they already exist
@@ -342,8 +307,6 @@ async function installDataStreamComponentTemplates(params: {
}
})
);
-
- return { componentTemplateNames: Object.keys(componentTemplates) };
}
export async function ensureDefaultComponentTemplates(
@@ -387,21 +350,15 @@ export async function ensureComponentTemplate(
return { isCreated: !existingTemplate };
}
-export async function installTemplate({
- esClient,
- logger,
- fields,
+export function prepareTemplate({
+ pkg,
dataStream,
- packageVersion,
- packageName,
}: {
- esClient: ElasticsearchClient;
- logger: Logger;
- fields: Field[];
+ pkg: Pick;
dataStream: RegistryDataStream;
- packageVersion: string;
- packageName: string;
-}): Promise {
+}): { componentTemplates: TemplateMap; indexTemplate: IndexTemplateEntry } {
+ const { name: packageName, version: packageVersion } = pkg;
+ const fields = loadFieldsFromYaml(pkg, dataStream.path);
const validFields = processFields(fields);
const mappings = generateMappings(validFields);
const templateName = generateTemplateName(dataStream);
@@ -425,40 +382,51 @@ export async function installTemplate({
ilmPolicy: dataStream.ilm_policy,
});
- const { componentTemplateNames } = await installDataStreamComponentTemplates({
+ const componentTemplates = buildComponentTemplates({
+ defaultSettings,
mappings,
+ packageName,
templateName,
registryElasticsearch: dataStream.elasticsearch,
- esClient,
- logger,
- packageName,
- defaultSettings,
});
const template = getTemplate({
templateIndexPattern,
pipelineName,
packageName,
- composedOfTemplates: componentTemplateNames,
+ composedOfTemplates: Object.keys(componentTemplates),
templatePriority,
hidden: dataStream.hidden,
});
+ return {
+ componentTemplates,
+ indexTemplate: {
+ templateName,
+ indexTemplate: template,
+ },
+ };
+}
+
+async function installTemplate({
+ esClient,
+ logger,
+ template,
+}: {
+ esClient: ElasticsearchClient;
+ logger: Logger;
+ template: IndexTemplateEntry;
+}) {
// TODO: Check return values for errors
const esClientParams = {
- name: templateName,
- body: template,
+ name: template.templateName,
+ body: template.indexTemplate,
};
await retryTransientEsErrors(
() => esClient.indices.putIndexTemplate(esClientParams, { ignore: [404] }),
{ logger }
);
-
- return {
- templateName,
- indexTemplate: template,
- };
}
export function getAllTemplateRefs(installedTemplates: IndexTemplateEntry[]) {
diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts
index 3f1a8d8b2b7ba..0e00840b0c74e 100644
--- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts
+++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts
@@ -261,12 +261,12 @@ const isFields = (path: string) => {
* Gets all field files, optionally filtered by dataset, extracts .yml files, merges them together
*/
-export const loadFieldsFromYaml = async (
+export const loadFieldsFromYaml = (
pkg: Pick,
datasetName?: string
-): Promise => {
+): Field[] => {
// Fetch all field definition files
- const fieldDefinitionFiles = await getAssetsData(pkg, isFields, datasetName);
+ const fieldDefinitionFiles = getAssetsData(pkg, isFields, datasetName);
return fieldDefinitionFiles.reduce((acc, file) => {
// Make sure it is defined as it is optional. Should never happen.
if (file.buffer) {
diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
index 1462cd61c4bd3..b9582ce1cf148 100644
--- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts
@@ -267,6 +267,7 @@ export async function installKibanaSavedObjects({
overwrite: true,
readStream: createListStream(toBeSavedObjects),
createNewCopies: false,
+ refresh: false,
})
);
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
index 24c324e6b7cd0..0124bff41736f 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
@@ -22,10 +22,10 @@ import {
import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
import type { AssetReference, Installation, InstallType } from '../../../types';
-import { installTemplates } from '../elasticsearch/template/install';
+import { prepareToInstallTemplates } from '../elasticsearch/template/install';
import { removeLegacyTemplates } from '../elasticsearch/template/remove_legacy';
import {
- installPipelines,
+ prepareToInstallPipelines,
isTopLevelPipeline,
deletePreviousPipelines,
} from '../elasticsearch/ingest_pipeline';
@@ -39,7 +39,7 @@ import { saveArchiveEntries } from '../archive/storage';
import { ConcurrentInstallOperationError } from '../../../errors';
import { packagePolicyService } from '../..';
-import { createInstallation } from './install';
+import { createInstallation, updateEsAssetReferences } from './install';
import { withPackageSpan } from './utils';
// this is only exported for testing
@@ -146,17 +146,45 @@ export async function _installPackage({
installMlModel(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences)
);
- // installs versionized pipelines without removing currently installed ones
- esReferences = await withPackageSpan('Install ingest pipelines', () =>
- installPipelines(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences)
+ /**
+ * In order to install assets in parallel, we need to split the preparation step from the installation step. This
+ * allows us to know which asset references are going to be installed so that we can save them on the packages
+ * SO before installation begins. In the case of a failure during installing any individual asset, we'll have the
+ * references necessary to remove any assets in that were successfully installed during the rollback phase.
+ *
+ * This split of prepare/install could be extended to all asset types. Besides performance, it also allows us to
+ * more easily write unit tests against the asset generation code without needing to mock ES responses.
+ */
+ const preparedIngestPipelines = prepareToInstallPipelines(packageInfo, paths);
+ const preparedIndexTemplates = prepareToInstallTemplates(packageInfo, paths, esReferences);
+
+ // Update the references for the templates and ingest pipelines together. Need to be done togther to avoid race
+ // conditions on updating the installed_es field at the same time
+ // These must be saved before we actually attempt to install the templates or pipelines so that we know what to
+ // cleanup in the case that a single asset fails to install.
+ esReferences = await updateEsAssetReferences(
+ savedObjectsClient,
+ packageInfo.name,
+ esReferences,
+ {
+ assetsToRemove: preparedIndexTemplates.assetsToRemove,
+ assetsToAdd: [
+ ...preparedIngestPipelines.assetsToAdd,
+ ...preparedIndexTemplates.assetsToAdd,
+ ],
+ }
);
- // install or update the templates referencing the newly installed pipelines
- const { installedTemplates, installedEsReferences: esReferencesAfterTemplates } =
- await withPackageSpan('Install index templates', () =>
- installTemplates(packageInfo, esClient, logger, paths, savedObjectsClient, esReferences)
- );
- esReferences = esReferencesAfterTemplates;
+ // Install index templates and ingest pipelines in parallel since they typically take the longest
+ const [installedTemplates] = await Promise.all([
+ withPackageSpan('Install index templates', () =>
+ preparedIndexTemplates.install(esClient, logger)
+ ),
+ // installs versionized pipelines without removing currently installed ones
+ withPackageSpan('Install ingest pipelines', () =>
+ preparedIngestPipelines.install(esClient, logger)
+ ),
+ ]);
try {
await removeLegacyTemplates({ packageInfo, esClient, logger });
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts
index 0621d05d21497..d67e76f90e551 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/assets.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/assets.ts
@@ -51,11 +51,11 @@ export function getAssets(
// ASK: Does getAssetsData need an installSource now?
// if so, should it be an Installation vs InstallablePackage or add another argument?
-export async function getAssetsData(
+export function getAssetsData(
packageInfo: Pick,
filter = (path: string): boolean => true,
datasetName?: string
-): Promise {
+): ArchiveEntry[] {
// Gather all asset data
const assets = getAssets(packageInfo, filter, datasetName);
const entries: ArchiveEntry[] = assets.map((path) => {
diff --git a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson
index b29c4e28e731d..0e1a2f4b67cac 100644
--- a/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson
+++ b/x-pack/plugins/osquery/cypress/fixtures/saved_objects/saved_query.ndjson
@@ -14,6 +14,7 @@
"id": "Saved-Query-Id",
"interval": "3600",
"query": "select * from uptime;",
+ "platform": "linux,darwin",
"updated_at": "2021-12-21T08:54:38.648Z",
"updated_by": "elastic"
},
diff --git a/x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts b/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts
similarity index 59%
rename from x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts
rename to x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts
index 1ce25a77f834a..6dde0013a4bc6 100644
--- a/x-pack/plugins/osquery/cypress/integration/all/delete_all_ecs_mappings.spec.ts
+++ b/x-pack/plugins/osquery/cypress/integration/all/edit_saved_queries.spec.ts
@@ -10,7 +10,7 @@ import { login } from '../../tasks/login';
import { ArchiverMethod, runKbnArchiverScript } from '../../tasks/archiver';
import { ROLES } from '../../test';
-describe('ALL - Delete ECS Mappings', () => {
+describe('ALL - Edit saved query', () => {
const SAVED_QUERY_ID = 'Saved-Query-Id';
before(() => {
@@ -25,7 +25,7 @@ describe('ALL - Delete ECS Mappings', () => {
runKbnArchiverScript(ArchiverMethod.UNLOAD, 'saved_query');
});
- it('to click the edit button and edit pack', () => {
+ it('by changing ecs mappings and platforms', () => {
cy.react('CustomItemAction', {
props: { index: 1, item: { attributes: { id: SAVED_QUERY_ID } } },
}).click();
@@ -35,7 +35,33 @@ describe('ALL - Delete ECS Mappings', () => {
.parents('[data-test-subj="ECSMappingEditorForm"]')
.react('EuiButtonIcon', { props: { iconType: 'trash' } })
.click();
+
+ cy.react('PlatformCheckBoxGroupField').within(() => {
+ cy.react('EuiCheckbox', {
+ props: {
+ id: 'linux',
+ checked: true,
+ },
+ }).should('exist');
+ cy.react('EuiCheckbox', {
+ props: {
+ id: 'darwin',
+ checked: true,
+ },
+ }).should('exist');
+
+ cy.react('EuiCheckbox', {
+ props: {
+ id: 'windows',
+ checked: false,
+ },
+ }).should('exist');
+ });
+
+ cy.get('#windows').check({ force: true });
+
cy.react('EuiButton').contains('Update query').click();
+
cy.wait(5000);
cy.react('CustomItemAction', {
@@ -43,5 +69,27 @@ describe('ALL - Delete ECS Mappings', () => {
}).click();
cy.contains('Custom key/value pairs').should('not.exist');
cy.contains('Hours of uptime').should('not.exist');
+
+ cy.react('PlatformCheckBoxGroupField').within(() => {
+ cy.react('EuiCheckbox', {
+ props: {
+ id: 'linux',
+ checked: true,
+ },
+ }).should('exist');
+ cy.react('EuiCheckbox', {
+ props: {
+ id: 'darwin',
+ checked: true,
+ },
+ }).should('exist');
+
+ cy.react('EuiCheckbox', {
+ props: {
+ id: 'windows',
+ checked: true,
+ },
+ }).should('exist');
+ });
});
});
diff --git a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx
index 6da252f78aedf..1d0d9f28d097b 100644
--- a/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx
+++ b/x-pack/plugins/osquery/public/saved_queries/form/use_saved_query_form.tsx
@@ -56,13 +56,6 @@ export const useSavedQueryForm = ({ defaultValue, handleSubmit }: UseSavedQueryF
defaultValue,
serializer: (payload) =>
produce(payload, (draft) => {
- // @ts-expect-error update types
- if (draft.platform?.split(',').length === 3) {
- // if all platforms are checked then use undefined
- // @ts-expect-error update types
- delete draft.platform;
- }
-
if (isArray(draft.version)) {
if (!draft.version.length) {
// @ts-expect-error update types
diff --git a/x-pack/plugins/screenshotting/common/index.ts b/x-pack/plugins/screenshotting/common/index.ts
index b6b9034cb8189..7570477a1c1c9 100644
--- a/x-pack/plugins/screenshotting/common/index.ts
+++ b/x-pack/plugins/screenshotting/common/index.ts
@@ -14,3 +14,5 @@ export {
SCREENSHOTTING_EXPRESSION,
SCREENSHOTTING_EXPRESSION_INPUT,
} from './expression';
+
+export const PLUGIN_ID = 'screenshotting';
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts
index 7a2453b2a426b..ce28c53bb5f88 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/index.ts
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/index.ts
@@ -5,13 +5,14 @@
* 2.0.
*/
-import { groupBy } from 'lodash';
import type { Values } from '@kbn/utility-types';
-import type { Logger, PackageInfo } from '@kbn/core/server';
+import { groupBy } from 'lodash';
+import type { PackageInfo } from '@kbn/core/server';
import type { LayoutParams } from '../../../common';
import { LayoutTypes } from '../../../common';
import type { Layout } from '../../layouts';
-import type { CaptureOptions, CaptureResult, CaptureMetrics } from '../../screenshots';
+import type { CaptureMetrics, CaptureOptions, CaptureResult } from '../../screenshots';
+import { EventLogger, Transactions } from '../../screenshots/event_logger';
import { pngsToPdf } from './pdf_maker';
/**
@@ -92,7 +93,7 @@ function getTimeRange(results: CaptureResult['results']) {
}
export async function toPdf(
- logger: Logger,
+ eventLogger: EventLogger,
packageInfo: PackageInfo,
layout: Layout,
{ logo, title }: PdfScreenshotOptions,
@@ -106,7 +107,7 @@ export async function toPdf(
layout,
logo,
packageInfo,
- logger,
+ eventLogger,
});
return {
@@ -119,7 +120,8 @@ export async function toPdf(
renderErrors: results.flatMap(({ renderErrors }) => renderErrors ?? []),
};
} catch (error) {
- logger.error(`Could not generate the PDF buffer!`);
+ eventLogger.kbnLogger.error(`Could not generate the PDF buffer!`);
+ eventLogger.error(error, Transactions.PDF);
throw error;
}
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts
index be69ec4c5e141..280b9173c7920 100644
--- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts
+++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/index.ts
@@ -5,17 +5,17 @@
* 2.0.
*/
-import type { Logger, PackageInfo } from '@kbn/core/server';
-import { PdfMaker } from './pdfmaker';
+import type { PackageInfo } from '@kbn/core/server';
import type { Layout } from '../../../layouts';
-import { getTracker } from './tracker';
import type { CaptureResult } from '../../../screenshots';
+import { Actions, EventLogger, Transactions } from '../../../screenshots/event_logger';
+import { PdfMaker } from './pdfmaker';
interface PngsToPdfArgs {
results: CaptureResult['results'];
layout: Layout;
packageInfo: PackageInfo;
- logger: Logger;
+ eventLogger: EventLogger;
logo?: string;
title?: string;
}
@@ -26,37 +26,43 @@ export async function pngsToPdf({
logo,
title,
packageInfo,
- logger,
+ eventLogger,
}: PngsToPdfArgs): Promise<{ buffer: Buffer; pages: number }> {
- const pdfMaker = new PdfMaker(layout, logo, packageInfo, logger);
- const tracker = getTracker();
- if (title) {
- pdfMaker.setTitle(title);
- }
- results.forEach((result) => {
- result.screenshots.forEach((png) => {
- tracker.startAddImage();
- pdfMaker.addImage(png.data, {
- title: png.title ?? undefined,
- description: png.description ?? undefined,
- });
- tracker.endAddImage();
- });
- });
+ const { kbnLogger } = eventLogger;
+ const transactionEnd = eventLogger.startTransaction(Transactions.PDF);
let buffer: Uint8Array | null = null;
+ let pdfMaker: PdfMaker | null = null;
try {
- tracker.startCompile();
+ pdfMaker = new PdfMaker(layout, logo, packageInfo, kbnLogger);
+ if (title) {
+ pdfMaker.setTitle(title);
+ }
+ results.forEach((result) => {
+ result.screenshots.forEach((png) => {
+ const spanEnd = eventLogger.logPdfEvent(
+ 'add image to PDF file',
+ Actions.ADD_IMAGE,
+ 'output'
+ );
+ pdfMaker?.addImage(png.data, {
+ title: png.title ?? undefined,
+ description: png.description ?? undefined,
+ });
+ spanEnd();
+ });
+ });
+
+ const spanEnd = eventLogger.logPdfEvent('compile PDF file', Actions.COMPILE, 'output');
buffer = await pdfMaker.generate();
- tracker.endCompile();
+ spanEnd();
const byteLength = buffer?.byteLength ?? 0;
- logger.debug(`PDF buffer byte length: ${byteLength}`);
- tracker.setByteLength(byteLength);
- } catch (err) {
- throw err;
- } finally {
- tracker.end();
+ transactionEnd({ labels: { byte_length_pdf: byteLength, pdf_pages: pdfMaker.getPageCount() } });
+ } catch (error) {
+ kbnLogger.error(error);
+ eventLogger.error(error, Actions.COMPILE);
+ throw error;
}
return { buffer: Buffer.from(buffer.buffer), pages: pdfMaker.getPageCount() };
diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts
deleted file mode 100644
index 49576a03d18a3..0000000000000
--- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/tracker.ts
+++ /dev/null
@@ -1,52 +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 apm from 'elastic-apm-node';
-
-interface PdfTracker {
- setByteLength: (byteLength: number) => void;
- startAddImage: () => void;
- endAddImage: () => void;
- startCompile: () => void;
- endCompile: () => void;
- end: () => void;
-}
-
-const TRANSACTION_TYPE = 'reporting'; // TODO: Find out whether we can rename to "screenshotting";
-const SPANTYPE_OUTPUT = 'output';
-
-interface ApmSpan {
- end: () => void;
-}
-
-export function getTracker(): PdfTracker {
- const apmTrans = apm.startTransaction('generate-pdf', TRANSACTION_TYPE);
-
- let apmAddImage: ApmSpan | null = null;
- let apmCompilePdf: ApmSpan | null = null;
-
- return {
- startAddImage() {
- apmAddImage = apmTrans?.startSpan('add-pdf-image', SPANTYPE_OUTPUT) || null;
- },
- endAddImage() {
- apmAddImage?.end();
- },
- startCompile() {
- apmCompilePdf = apmTrans?.startSpan('compile-pdf', SPANTYPE_OUTPUT) || null;
- },
- endCompile() {
- apmCompilePdf?.end();
- },
- setByteLength(byteLength: number) {
- apmTrans?.setLabel('byte-length', byteLength, false);
- },
- end() {
- apmTrans?.end();
- },
- };
-}
diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts
index 27da8b3430e6d..144b88a2c1c75 100755
--- a/x-pack/plugins/screenshotting/server/plugin.ts
+++ b/x-pack/plugins/screenshotting/server/plugin.ts
@@ -85,11 +85,9 @@ export class ScreenshottingPlugin implements Plugin {
const browserDriverFactory = await this.browserDriverFactory;
- const logger = this.logger.get('screenshot');
-
return new Screenshots(
browserDriverFactory,
- logger,
+ this.logger,
this.packageInfo,
http,
this.config,
diff --git a/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap b/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap
index c0971c9b95763..1b3826ce9980d 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap
+++ b/x-pack/plugins/screenshotting/server/screenshots/__snapshots__/index.test.ts.snap
@@ -20,7 +20,7 @@ Array [
},
},
],
- "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Mock error!],
+ "error": [Error: The "wait for elements" phase encountered an error: Error: Mock error!],
"renderErrors": undefined,
"screenshots": Array [
Object {
@@ -63,7 +63,7 @@ Array [
},
},
],
- "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Mock error!],
+ "error": [Error: The "wait for elements" phase encountered an error: Error: Mock error!],
"renderErrors": undefined,
"screenshots": Array [
Object {
diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts
new file mode 100644
index 0000000000000..3a20c404ff497
--- /dev/null
+++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.test.ts
@@ -0,0 +1,261 @@
+/*
+ * 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 moment from 'moment';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
+import { Actions, EventLogger, ScreenshottingAction, Transactions } from '.';
+import { ElementPosition } from '../get_element_position_data';
+import { ConfigType } from '../../config';
+
+jest.mock('uuid', () => ({
+ v4: () => 'NEW_UUID',
+}));
+
+type EventLoggerArgs = [message: string, meta: ScreenshottingAction];
+describe('Event Logger', () => {
+ let eventLogger: EventLogger;
+ let config: ConfigType;
+ let logSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ const testDate = moment(new Date('2021-04-12T16:00:00.000Z'));
+ let delaySeconds = 1;
+
+ jest.spyOn(global.Date, 'now').mockImplementation(() => {
+ return testDate.add(delaySeconds++, 'seconds').valueOf();
+ });
+
+ const logger = loggingSystemMock.createLogger();
+ config = { capture: { zoom: 2 } } as ConfigType;
+ eventLogger = new EventLogger(logger, config);
+
+ logSpy = jest.spyOn(logger, 'debug') as jest.SpyInstance;
+ });
+
+ it('creates logs for the events and includes durations and event payload data', () => {
+ const screenshottingEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING);
+ const openUrlEnd = eventLogger.logScreenshottingEvent(
+ 'open the url to the Kibana application',
+ Actions.OPEN_URL,
+ 'wait'
+ );
+ openUrlEnd();
+ const getElementPositionsEnd = eventLogger.logScreenshottingEvent(
+ 'scan the page to find the boundaries of visualization elements',
+ Actions.GET_ELEMENT_POSITION_DATA,
+ 'wait'
+ );
+ getElementPositionsEnd();
+ screenshottingEnd({
+ labels: {
+ cpu: 12,
+ cpu_percentage: 0,
+ memory: 450789,
+ memory_mb: 449,
+ byte_length: 14000,
+ },
+ });
+
+ const pdfEnd = eventLogger.startTransaction(Transactions.PDF);
+ const addImageEnd = eventLogger.logPdfEvent(
+ 'add image to the PDF file',
+ Actions.ADD_IMAGE,
+ 'output'
+ );
+ addImageEnd();
+ pdfEnd({ labels: { pdf_pages: 1, byte_length_pdf: 6666 } });
+
+ const logs = logSpy.mock.calls.map(([message, data]) => ({
+ message,
+ duration: data?.event?.duration,
+ screenshotting: data?.kibana?.screenshotting,
+ }));
+
+ expect(logs.length).toBe(10);
+ expect(logs).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "duration": undefined,
+ "message": "starting: screenshot-pipeline",
+ "screenshotting": Object {
+ "action": "screenshot-pipeline-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": undefined,
+ "message": "starting: open the url to the Kibana application",
+ "screenshotting": Object {
+ "action": "open-url-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": 3000,
+ "message": "completed: open the url to the Kibana application",
+ "screenshotting": Object {
+ "action": "open-url-complete",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": undefined,
+ "message": "starting: scan the page to find the boundaries of visualization elements",
+ "screenshotting": Object {
+ "action": "get-element-position-data-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": 5000,
+ "message": "completed: scan the page to find the boundaries of visualization elements",
+ "screenshotting": Object {
+ "action": "get-element-position-data-complete",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": 20000,
+ "message": "completed: screenshot-pipeline",
+ "screenshotting": Object {
+ "action": "screenshot-pipeline-complete",
+ "byte_length": 14000,
+ "cpu": 12,
+ "cpu_percentage": 0,
+ "memory": 450789,
+ "memory_mb": 449,
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": undefined,
+ "message": "starting: generate-pdf",
+ "screenshotting": Object {
+ "action": "generate-pdf-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": undefined,
+ "message": "starting: add image to the PDF file",
+ "screenshotting": Object {
+ "action": "add-pdf-image-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": 9000,
+ "message": "completed: add image to the PDF file",
+ "screenshotting": Object {
+ "action": "add-pdf-image-complete",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": 27000,
+ "message": "completed: generate-pdf",
+ "screenshotting": Object {
+ "action": "generate-pdf-complete",
+ "byte_length_pdf": 6666,
+ "pdf_pages": 1,
+ "session_id": "NEW_UUID",
+ },
+ },
+ ]
+ `);
+ });
+
+ it('logs the number of pixels', () => {
+ const elementPosition = {
+ boundingClientRect: { width: 1350, height: 2000 },
+ scroll: {},
+ } as ElementPosition;
+ const endScreenshot = eventLogger.logScreenshottingEvent(
+ 'screenshot capture test',
+ Actions.GET_SCREENSHOT,
+ 'read',
+ eventLogger.getPixelsFromElementPosition(elementPosition)
+ );
+ endScreenshot({ byte_length: 4444 });
+
+ const logData = logSpy.mock.calls.map(([message, data]) => ({
+ message,
+ duration: data.event?.duration,
+ screenshotting: data.kibana.screenshotting,
+ }));
+
+ expect(logData).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "duration": undefined,
+ "message": "starting: screenshot capture test",
+ "screenshotting": Object {
+ "action": "get-screenshots-start",
+ "pixels": 10800000,
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "duration": 2000,
+ "message": "completed: screenshot capture test",
+ "screenshotting": Object {
+ "action": "get-screenshots-complete",
+ "byte_length": 4444,
+ "pixels": 10800000,
+ "session_id": "NEW_UUID",
+ },
+ },
+ ]
+ `);
+ });
+
+ it('creates helpful error logs', () => {
+ eventLogger.startTransaction(Transactions.SCREENSHOTTING);
+ eventLogger.logScreenshottingEvent('opening the url', Actions.OPEN_URL, 'wait');
+ eventLogger.error(new Error('Something erroneous happened'), Transactions.SCREENSHOTTING);
+
+ const logData = logSpy.mock.calls.map(([message, data]) => ({
+ message,
+ error: data.error,
+ screenshotting: data.kibana.screenshotting,
+ }));
+
+ expect(logData).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "error": undefined,
+ "message": "starting: screenshot-pipeline",
+ "screenshotting": Object {
+ "action": "screenshot-pipeline-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "error": undefined,
+ "message": "starting: opening the url",
+ "screenshotting": Object {
+ "action": "open-url-start",
+ "session_id": "NEW_UUID",
+ },
+ },
+ Object {
+ "error": Object {
+ "code": undefined,
+ "message": "Something erroneous happened",
+ "stack_trace": undefined,
+ "type": undefined,
+ },
+ "message": "Error: Something erroneous happened",
+ "screenshotting": Object {
+ "action": "screenshot-pipeline-error",
+ "session_id": "NEW_UUID",
+ },
+ },
+ ]
+ `);
+ });
+});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts
new file mode 100644
index 0000000000000..033fb24c80685
--- /dev/null
+++ b/x-pack/plugins/screenshotting/server/screenshots/event_logger/index.ts
@@ -0,0 +1,315 @@
+/*
+ * 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, LogMeta } from '@kbn/core/server';
+import apm from 'elastic-apm-node';
+import uuid from 'uuid';
+import { CaptureResult } from '..';
+import { PLUGIN_ID } from '../../../common';
+import { ConfigType } from '../../config';
+import { ElementPosition } from '../get_element_position_data';
+import { Screenshot } from '../get_screenshots';
+
+export enum Actions {
+ OPEN_URL = 'open-url',
+ GET_ELEMENT_POSITION_DATA = 'get-element-position-data',
+ GET_NUMBER_OF_ITEMS = 'get-number-of-items',
+ GET_RENDER_ERRORS = 'get-render-errors',
+ GET_TIMERANGE = 'get-timerange',
+ INJECT_CSS = 'inject-css',
+ REPOSITION = 'position-elements',
+ WAIT_RENDER = 'wait-for-render',
+ WAIT_VISUALIZATIONS = 'wait-for-visualizations',
+ GET_SCREENSHOT = 'get-screenshots',
+ ADD_IMAGE = 'add-pdf-image',
+ COMPILE = 'compile-pdf',
+}
+
+export enum Transactions {
+ SCREENSHOTTING = 'screenshot-pipeline',
+ PDF = 'generate-pdf',
+}
+
+export type SpanTypes = 'setup' | 'read' | 'wait' | 'correction' | 'output';
+
+export interface ScreenshottingAction extends LogMeta {
+ event?: {
+ duration?: number; // number of nanoseconds from begin to end of an event
+ provider: typeof PLUGIN_ID;
+ };
+
+ message: string;
+ kibana: {
+ screenshotting: {
+ action: Actions | Transactions;
+ session_id: string;
+
+ // chromium stats
+ cpu?: number;
+ cpu_percentage?: number;
+ memory?: number;
+ memory_mb?: number;
+
+ // screenshotting stats
+ items_count?: number;
+ pixels?: number;
+ byte_length?: number;
+ element_positions?: number;
+ render_errors?: number;
+
+ // pdf stats
+ byte_length_pdf?: number;
+ pdf_pages?: number;
+ };
+ };
+}
+
+interface ErrorAction {
+ message: string;
+ code?: string;
+ stack_trace?: string;
+ type?: string;
+}
+
+type SimpleEvent = Omit;
+
+type LogAdapter = (
+ message: string,
+ suffix: 'start' | 'complete' | 'error',
+ event: Partial,
+ startTime?: Date | undefined
+) => void;
+
+type Labels = Record;
+type TransactionEndFn = (args: { labels: Partial }) => void;
+type LogEndFn = (metricData?: Partial) => void;
+
+function fillLogData(
+ message: string,
+ event: Partial,
+ suffix: 'start' | 'complete' | 'error',
+ sessionId: string,
+ duration: number | undefined
+) {
+ let newMessage = message;
+ if (suffix !== 'error') {
+ newMessage = `${suffix === 'start' ? 'starting' : 'completed'}: ${message}`;
+ }
+
+ let interpretedAction: string;
+ if (suffix === 'error') {
+ interpretedAction = event.action + '-error';
+ } else {
+ interpretedAction = event.action + `-${suffix}`;
+ }
+
+ const logData: ScreenshottingAction = {
+ message: newMessage,
+ kibana: {
+ screenshotting: {
+ ...event,
+ action: interpretedAction as Actions,
+ session_id: sessionId,
+ },
+ },
+ event: { duration, provider: PLUGIN_ID },
+ };
+ return logData;
+}
+
+function logAdapter(logger: Logger, sessionId: string) {
+ const log: LogAdapter = (message, suffix, event, startTime) => {
+ let duration: number | undefined;
+ if (startTime != null) {
+ const start = startTime.valueOf();
+ duration = new Date(Date.now()).valueOf() - start.valueOf();
+ }
+
+ const logData = fillLogData(message, event, suffix, sessionId, duration);
+ logger.debug(logData.message, logData);
+ };
+ return log;
+}
+
+/**
+ * A class to use internal state properties to log timing between actions in the screenshotting pipeline
+ */
+export class EventLogger {
+ private spans = new Map();
+ private transactions: Record = {
+ 'screenshot-pipeline': null,
+ 'generate-pdf': null,
+ };
+
+ private sessionId: string; // identifier to track all logs from one screenshotting flow
+ private logEvent: LogAdapter;
+ private timings: Partial> = {};
+
+ constructor(private readonly logger: Logger, private readonly config: ConfigType) {
+ this.sessionId = uuid.v4();
+ this.logEvent = logAdapter(logger.get('events'), this.sessionId);
+ }
+
+ private startTiming(a: Actions | Transactions) {
+ this.timings[a] = new Date(Date.now());
+ }
+
+ /**
+ * @returns Logger - original logger
+ */
+ public get kbnLogger() {
+ return this.logger;
+ }
+
+ /**
+ * General method for logging the beginning of any of this plugin's pipeline
+ *
+ * @returns {ScreenshottingEndFn}
+ */
+ public startTransaction(
+ action: Transactions.SCREENSHOTTING | Transactions.PDF
+ ): TransactionEndFn {
+ this.transactions[action] = apm.startTransaction(action, PLUGIN_ID);
+ const transaction = this.transactions[action];
+
+ this.startTiming(action);
+ this.logEvent(action, 'start', { action });
+
+ return ({ labels }) => {
+ Object.entries(labels).forEach(([label]) => {
+ const labelField = label as keyof SimpleEvent;
+ const labelValue = labels[labelField];
+ transaction?.setLabel(label, labelValue, false);
+ });
+
+ transaction?.end();
+
+ this.logEvent(action, 'complete', { ...labels, action }, this.timings[action]);
+ };
+ }
+
+ /**
+ * General event logging function
+ *
+ * @param {string} message
+ * @param {Actions} action - action type for kibana.screenshotting.action
+ * @param {TransactionType} transaction - name of the internal APM transaction in which to associate the span
+ * @param {SpanTypes} type - identifier of the span type
+ * @param {metricsPre} type - optional metrics to add to the "start" log of the event
+ * @returns {LogEndFn} - function to log the end of the span
+ */
+ public log(
+ message: string,
+ action: Actions,
+ type: SpanTypes,
+ metricsPre: Partial = {},
+ transaction: Transactions
+ ): LogEndFn {
+ const txn = this.transactions[transaction];
+ const span = txn?.startSpan(action, type);
+
+ this.spans.set(action, span);
+ this.startTiming(action);
+ this.logEvent(message, 'start', { ...metricsPre, action });
+
+ return (metricData = {}) => {
+ span?.end();
+ this.logEvent(
+ message,
+ 'complete',
+ { ...metricsPre, ...metricData, action },
+ this.timings[action]
+ );
+ };
+ }
+
+ /**
+ * Logging helper for screenshotting events
+ */
+ public logScreenshottingEvent(
+ message: string,
+ action: Actions,
+ type: SpanTypes,
+ metricsPre: Partial = {}
+ ) {
+ return this.log(message, action, type, metricsPre, Transactions.SCREENSHOTTING);
+ }
+
+ /**
+ * Logging helper for screenshotting events
+ */
+ public logPdfEvent(
+ message: string,
+ action: Actions,
+ type: SpanTypes,
+ metricsPre: Partial = {}
+ ) {
+ return this.log(message, action, type, metricsPre, Transactions.PDF);
+ }
+
+ /**
+ * Helper function to calculate the byte length of a set of captured PNG images
+ */
+ public getByteLengthFromCaptureResults(
+ results: CaptureResult['results']
+ ): Pick {
+ const totalByteLength = results.reduce(
+ (totals, { screenshots }) =>
+ totals +
+ screenshots.reduce(
+ (byteLength: number, screenshot: Screenshot) => byteLength + screenshot.data.byteLength,
+ 0
+ ),
+ 0
+ );
+ return { byte_length: totalByteLength };
+ }
+
+ /**
+ * Helper function to create the "metricPre" data needed to log the start
+ * of a screenshot capture event.
+ */
+ public getPixelsFromElementPosition(
+ elementPosition: ElementPosition
+ ): Pick {
+ const { width, height } = elementPosition.boundingClientRect;
+ const zoom = this.config.capture.zoom;
+ const pixels = width * zoom * (height * zoom);
+ return { pixels };
+ }
+
+ /**
+ * General error logger
+ *
+ * @param {ErrorAction} error: The error object that was caught
+ * @param {Actions} action: The screenshotting action type
+ * @returns void
+ */
+ public error(error: ErrorAction | string, action: Actions | Transactions) {
+ const isError = typeof error === 'object';
+ const message = `Error: ${isError ? error.message : error}`;
+
+ const errorData = {
+ ...fillLogData(
+ message,
+ { action },
+ 'error',
+ this.sessionId,
+ undefined //
+ ),
+ error: {
+ message: isError ? error.message : error,
+ code: isError ? error.code : undefined,
+ stack_trace: isError ? error.stack_trace : undefined,
+ type: isError ? error.type : undefined,
+ },
+ };
+
+ this.logger.get('events').debug(message, errorData);
+ apm.captureError(error as Error | string);
+ }
+}
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts
index 915b036acf22e..f3a76ca79d85f 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts
@@ -5,20 +5,24 @@
* 2.0.
*/
-import type { Logger } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
+import { ConfigType } from '../config';
import { createMockLayout } from '../layouts/mock';
+import { EventLogger } from './event_logger';
import { getElementPositionAndAttributes } from './get_element_position_data';
describe('getElementPositionAndAttributes', () => {
- const logger = {} as jest.Mocked;
let browser: ReturnType;
let layout: ReturnType;
+ let eventLogger: EventLogger;
+ let config = {} as ConfigType;
beforeEach(async () => {
browser = createMockBrowserDriver();
layout = createMockLayout();
-
+ config = { capture: { zoom: 2 } } as ConfigType;
+ eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
// @see https://github.com/jsdom/jsdom/issues/653
@@ -59,7 +63,7 @@ describe('getElementPositionAndAttributes', () => {
/>
`;
- await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves
+ await expect(getElementPositionAndAttributes(browser, eventLogger, layout)).resolves
.toMatchInlineSnapshot(`
Array [
Object {
@@ -103,6 +107,6 @@ describe('getElementPositionAndAttributes', () => {
});
it('should return null when there are no elements matching', async () => {
- await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves.toBeNull();
+ await expect(getElementPositionAndAttributes(browser, eventLogger, layout)).resolves.toBeNull();
});
});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts
index e3235c6d23253..5018701ce2411 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts
@@ -5,11 +5,10 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { CONTEXT_ELEMENTATTRIBUTES } from './constants';
+import { Actions, EventLogger } from './event_logger';
export interface AttributesMap {
[key: string]: string | null;
@@ -36,10 +35,17 @@ export interface ElementsPositionAndAttribute {
export const getElementPositionAndAttributes = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
layout: Layout
): Promise => {
- const span = apm.startSpan('get_element_position_data', 'read');
+ const { kbnLogger } = eventLogger;
+
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'get element position data',
+ Actions.GET_ELEMENT_POSITION_DATA,
+ 'read'
+ );
+
const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container
let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null;
try {
@@ -77,7 +83,7 @@ export const getElementPositionAndAttributes = async (
args: [screenshotSelector, { title: 'data-title', description: 'data-description' }],
},
{ context: CONTEXT_ELEMENTATTRIBUTES },
- logger
+ kbnLogger
);
if (!elementsPositionAndAttributes?.length) {
@@ -86,10 +92,13 @@ export const getElementPositionAndAttributes = async (
);
}
} catch (err) {
+ kbnLogger.error(err);
+ eventLogger.error(err, Actions.GET_ELEMENT_POSITION_DATA);
elementsPositionAndAttributes = null;
+ // no throw
}
- span?.end();
+ spanEnd({ element_positions: elementsPositionAndAttributes?.length });
return elementsPositionAndAttributes;
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts
index 34b8291eb03da..a7c4f27065bcf 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts
@@ -5,22 +5,25 @@
* 2.0.
*/
-import type { Logger } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
+import { ConfigType } from '../config';
import { createMockLayout } from '../layouts/mock';
+import { EventLogger } from './event_logger';
import { getNumberOfItems } from './get_number_of_items';
describe('getNumberOfItems', () => {
const timeout = 10;
let browser: ReturnType;
let layout: ReturnType;
- let logger: jest.Mocked;
+ let eventLogger: EventLogger;
+ let config = {} as ConfigType;
beforeEach(async () => {
browser = createMockBrowserDriver();
layout = createMockLayout();
- logger = { debug: jest.fn() } as unknown as jest.Mocked;
-
+ config = { capture: { zoom: 2 } } as ConfigType;
+ eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
});
@@ -33,7 +36,7 @@ describe('getNumberOfItems', () => {
`;
- await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(10);
+ await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(10);
});
it('should determine the number of items by selector ', async () => {
@@ -43,7 +46,7 @@ describe('getNumberOfItems', () => {
`;
- await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(3);
+ await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(3);
});
it('should fall back to the selector when the attribute is empty', async () => {
@@ -53,6 +56,6 @@ describe('getNumberOfItems', () => {
`;
- await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(2);
+ await expect(getNumberOfItems(browser, eventLogger, timeout, layout)).resolves.toBe(2);
});
});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts
index 9dab001e4730d..0e4da2fe5cf6a 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts
@@ -5,24 +5,27 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants';
+import { Actions, EventLogger } from './event_logger';
export const getNumberOfItems = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
timeout: number,
layout: Layout
): Promise => {
- const span = apm.startSpan('get_number_of_items', 'read');
+ const { kbnLogger } = eventLogger;
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'get the number of visualization items on the page',
+ Actions.GET_NUMBER_OF_ITEMS,
+ 'read'
+ );
+
const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors;
let itemsCount: number;
- logger.debug('waiting for elements or items count attribute; or not found to interrupt');
-
try {
// the dashboard is using the `itemsCountAttribute` attribute to let us
// know how many items to expect since gridster incrementally adds panels
@@ -31,7 +34,7 @@ export const getNumberOfItems = async (
`${renderCompleteSelector},[${itemsCountAttribute}]`,
{ timeout },
{ context: CONTEXT_READMETADATA },
- logger
+ kbnLogger
);
// returns the value of the `itemsCountAttribute` if it's there, otherwise
@@ -52,16 +55,15 @@ export const getNumberOfItems = async (
args: [renderCompleteSelector, itemsCountAttribute],
},
{ context: CONTEXT_GETNUMBEROFITEMS },
- logger
+ kbnLogger
);
} catch (error) {
- logger.error(error);
- throw new Error(
- `An error occurred when trying to read the page for visualization panel info: ${error.message}`
- );
+ kbnLogger.error(error);
+ eventLogger.error(error, Actions.GET_NUMBER_OF_ITEMS);
+ throw error;
}
- span?.end();
+ spanEnd({ items_count: itemsCount });
return itemsCount;
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts
index a475e3c614c15..ece25b37725c8 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts
@@ -5,21 +5,24 @@
* 2.0.
*/
-import type { Logger } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
+import { ConfigType } from '../config';
import { createMockLayout } from '../layouts/mock';
+import { EventLogger } from './event_logger';
import { getRenderErrors } from './get_render_errors';
describe('getRenderErrors', () => {
let browser: ReturnType;
let layout: ReturnType;
- let logger: jest.Mocked;
+ let eventLogger: EventLogger;
+ let config = {} as ConfigType;
beforeEach(async () => {
browser = createMockBrowserDriver();
layout = createMockLayout();
- logger = { debug: jest.fn(), warn: jest.fn() } as unknown as jest.Mocked;
-
+ config = { capture: { zoom: 2 } } as ConfigType;
+ eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
});
@@ -35,7 +38,7 @@ describe('getRenderErrors', () => {
`;
- await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual([
+ await expect(getRenderErrors(browser, eventLogger, layout)).resolves.toEqual([
'a test error',
'a test error',
'a test error',
@@ -48,6 +51,6 @@ describe('getRenderErrors', () => {
`;
- await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual(undefined);
+ await expect(getRenderErrors(browser, eventLogger, layout)).resolves.toEqual(undefined);
});
});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts
index e8beb18911210..44b92ceddbc8d 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts
@@ -5,45 +5,59 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import type { Layout } from '../layouts';
import { CONTEXT_GETRENDERERRORS } from './constants';
+import { Actions, EventLogger } from './event_logger';
export const getRenderErrors = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
layout: Layout
): Promise => {
- const span = apm.startSpan('get_render_errors', 'read');
- logger.debug('reading render errors');
- const errorsFound: undefined | string[] = await browser.evaluate(
- {
- fn: (errorSelector, errorAttribute) => {
- const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector));
- const errors: string[] = [];
-
- visualizations.forEach((visualization) => {
- const errorMessage = visualization.getAttribute(errorAttribute);
- if (errorMessage) {
- errors.push(errorMessage);
- }
- });
-
- return errors.length ? errors : undefined;
- },
- args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute],
- },
- { context: CONTEXT_GETRENDERERRORS },
- logger
+ const { kbnLogger } = eventLogger;
+
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'look for render errors',
+ Actions.GET_RENDER_ERRORS,
+ 'read'
);
- span?.end();
- if (errorsFound?.length) {
- logger.warn(
- `Found ${errorsFound.length} error messages. See report object for more information.`
+ let errorsFound: undefined | string[];
+ try {
+ errorsFound = await browser.evaluate(
+ {
+ fn: (errorSelector, errorAttribute) => {
+ const visualizations: Element[] = Array.from(document.querySelectorAll(errorSelector));
+ const errors: string[] = [];
+
+ visualizations.forEach((visualization) => {
+ const errorMessage = visualization.getAttribute(errorAttribute);
+ if (errorMessage) {
+ errors.push(errorMessage);
+ }
+ });
+
+ return errors.length ? errors : undefined;
+ },
+ args: [layout.selectors.renderError, layout.selectors.renderErrorAttribute],
+ },
+ { context: CONTEXT_GETRENDERERRORS },
+ kbnLogger
);
+
+ const renderErrors = errorsFound?.length;
+ if (renderErrors) {
+ kbnLogger.warn(
+ `Found ${renderErrors} error messages. See report object for more information.`
+ );
+ }
+
+ spanEnd({ render_errors: renderErrors });
+ } catch (error) {
+ kbnLogger.error(error);
+ eventLogger.error(error, Actions.GET_RENDER_ERRORS);
+ throw error;
}
return errorsFound;
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts
index 1f104b9bf2d80..c2342280aea20 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts
@@ -5,8 +5,10 @@
* 2.0.
*/
-import type { Logger } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
+import { ConfigType } from '../config';
+import { EventLogger } from './event_logger';
import { getScreenshots } from './get_screenshots';
describe('getScreenshots', () => {
@@ -27,12 +29,13 @@ describe('getScreenshots', () => {
},
];
let browser: ReturnType;
- let logger: jest.Mocked;
+ let eventLogger: EventLogger;
+ let config = {} as ConfigType;
beforeEach(async () => {
browser = createMockBrowserDriver();
- logger = { info: jest.fn() } as unknown as jest.Mocked;
-
+ config = { capture: { zoom: 2 } } as ConfigType;
+ eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
});
@@ -41,7 +44,7 @@ describe('getScreenshots', () => {
});
it('should return screenshots', async () => {
- await expect(getScreenshots(browser, logger, elementsPositionAndAttributes)).resolves
+ await expect(getScreenshots(browser, eventLogger, elementsPositionAndAttributes)).resolves
.toMatchInlineSnapshot(`
Array [
Object {
@@ -87,7 +90,7 @@ describe('getScreenshots', () => {
});
it('should forward elements positions', async () => {
- await getScreenshots(browser, logger, elementsPositionAndAttributes);
+ await getScreenshots(browser, eventLogger, elementsPositionAndAttributes);
expect(browser.screenshot).toHaveBeenCalledTimes(2);
expect(browser.screenshot).toHaveBeenNthCalledWith(
@@ -104,7 +107,7 @@ describe('getScreenshots', () => {
browser.screenshot.mockResolvedValue(Buffer.from(''));
await expect(
- getScreenshots(browser, logger, elementsPositionAndAttributes)
+ getScreenshots(browser, eventLogger, elementsPositionAndAttributes)
).rejects.toBeInstanceOf(Error);
});
});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts
index 53829b098ee8c..f157649bbb848 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts
@@ -5,9 +5,8 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
+import { Actions, EventLogger } from './event_logger';
import type { ElementsPositionAndAttribute } from './get_element_position_data';
export interface Screenshot {
@@ -29,33 +28,45 @@ export interface Screenshot {
export const getScreenshots = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
elementsPositionAndAttributes: ElementsPositionAndAttribute[]
): Promise => {
- logger.info(`taking screenshots`);
+ const { kbnLogger } = eventLogger;
+ kbnLogger.info(`taking screenshots`);
const screenshots: Screenshot[] = [];
- for (let i = 0; i < elementsPositionAndAttributes.length; i++) {
- const span = apm.startSpan('get_screenshots', 'read');
- const item = elementsPositionAndAttributes[i];
+ try {
+ for (let i = 0; i < elementsPositionAndAttributes.length; i++) {
+ const item = elementsPositionAndAttributes[i];
+ const endScreenshot = eventLogger.logScreenshottingEvent(
+ 'screenshot capture',
+ Actions.GET_SCREENSHOT,
+ 'read',
+ eventLogger.getPixelsFromElementPosition(item.position)
+ );
- const data = await browser.screenshot(item.position);
+ const data = await browser.screenshot(item.position);
- if (!data?.byteLength) {
- throw new Error(`Failure in getScreenshots! Screenshot data is void`);
- }
+ if (!data?.byteLength) {
+ throw new Error(`Failure in getScreenshots! Screenshot data is void`);
+ }
- screenshots.push({
- data,
- title: item.attributes.title,
- description: item.attributes.description,
- });
+ screenshots.push({
+ data,
+ title: item.attributes.title,
+ description: item.attributes.description,
+ });
- span?.end();
+ endScreenshot({ byte_length: data.byteLength });
+ }
+ } catch (error) {
+ kbnLogger.error(error);
+ eventLogger.error(error, Actions.GET_SCREENSHOT);
+ throw error;
}
- logger.info(`screenshots taken: ${screenshots.length}`);
+ kbnLogger.info(`screenshots taken: ${screenshots.length}`);
return screenshots;
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts
index 8484412f5fd94..a7a7b9295068e 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts
@@ -5,21 +5,24 @@
* 2.0.
*/
-import type { Logger } from '@kbn/core/server';
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { createMockBrowserDriver } from '../browsers/mock';
+import { ConfigType } from '../config';
import { createMockLayout } from '../layouts/mock';
+import { EventLogger } from './event_logger';
import { getTimeRange } from './get_time_range';
describe('getTimeRange', () => {
let browser: ReturnType;
let layout: ReturnType;
- let logger: jest.Mocked;
+ let eventLogger: EventLogger;
+ let config = {} as ConfigType;
beforeEach(async () => {
browser = createMockBrowserDriver();
layout = createMockLayout();
- logger = { debug: jest.fn(), info: jest.fn() } as unknown as jest.Mocked;
-
+ config = { capture: { zoom: 2 } } as ConfigType;
+ eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args));
});
@@ -28,7 +31,7 @@ describe('getTimeRange', () => {
});
it('should return null when there is no duration element', async () => {
- await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull();
+ await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBeNull();
});
it('should return null when duration attrbute is empty', async () => {
@@ -36,7 +39,7 @@ describe('getTimeRange', () => {
`;
- await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull();
+ await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBeNull();
});
it('should return duration', async () => {
@@ -44,6 +47,6 @@ describe('getTimeRange', () => {
`;
- await expect(getTimeRange(browser, logger, layout)).resolves.toBe('10');
+ await expect(getTimeRange(browser, eventLogger, layout)).resolves.toBe('10');
});
});
diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts
index 41d902436d36b..f9272fd27ac95 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts
@@ -5,19 +5,21 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { CONTEXT_GETTIMERANGE } from './constants';
+import { Actions, EventLogger } from './event_logger';
export const getTimeRange = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
layout: Layout
): Promise => {
- const span = apm.startSpan('get_time_range', 'read');
- logger.debug('getting timeRange');
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'looking for time range',
+ Actions.GET_TIMERANGE,
+ 'read'
+ );
const timeRange = await browser.evaluate(
{
@@ -38,16 +40,14 @@ export const getTimeRange = async (
args: [layout.selectors.timefilterDurationAttribute],
},
{ context: CONTEXT_GETTIMERANGE },
- logger
+ eventLogger.kbnLogger
);
if (timeRange) {
- logger.info(`timeRange: ${timeRange}`);
- } else {
- logger.debug('no timeRange');
+ eventLogger.kbnLogger.info(`timeRange: ${timeRange}`);
}
- span?.end();
+ spanEnd();
return timeRange;
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts
index b98270547dbec..33404bb5fadc2 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/index.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts
@@ -8,8 +8,6 @@
import type { HttpServiceSetup, KibanaRequest, Logger, PackageInfo } from '@kbn/core/server';
import type { ExpressionAstExpression } from '@kbn/expressions-plugin/common';
import type { Optional } from '@kbn/utility-types';
-import type { Transaction } from 'elastic-apm-node';
-import apm from 'elastic-apm-node';
import ipaddr from 'ipaddr.js';
import { defaultsDeep, sum } from 'lodash';
import { from, Observable, of, throwError } from 'rxjs';
@@ -46,6 +44,7 @@ import {
} from '../formats';
import type { Layout } from '../layouts';
import { createLayout } from '../layouts';
+import { EventLogger, Transactions } from './event_logger';
import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable';
import { ScreenshotObservableHandler, UrlOrUrlWithContext } from './observable';
import { Semaphore } from './semaphore';
@@ -110,21 +109,18 @@ export class Screenshots {
this.semaphore = new Semaphore(config.poolSize);
}
- private createLayout(transaction: Transaction | null, options: CaptureOptions): Layout {
- const apmCreateLayout = transaction?.startSpan('create-layout', 'setup');
+ private createLayout(options: CaptureOptions): Layout {
const layout = createLayout(options.layout ?? {});
this.logger.debug(`Layout: width=${layout.width} height=${layout.height}`);
- apmCreateLayout?.end();
return layout;
}
private captureScreenshots(
+ eventLogger: EventLogger,
layout: Layout,
- transaction: Transaction | null,
options: ScreenshotObservableOptions
): Observable {
- const apmCreatePage = transaction?.startSpan('create-page', 'wait');
const { browserTimezone } = options;
return this.browserDriverFactory
@@ -139,24 +135,22 @@ export class Screenshots {
.pipe(
this.semaphore.acquire(),
mergeMap(({ driver, unexpectedExit$, close }) => {
- apmCreatePage?.end();
- unexpectedExit$.subscribe({ error: () => transaction?.end() });
-
const screen = new ScreenshotObservableHandler(
driver,
this.config,
- this.logger,
+ eventLogger,
layout,
options
);
return from(options.urls).pipe(
concatMap((url, index) =>
- screen.setupPage(index, url, transaction).pipe(
+ screen.setupPage(index, url).pipe(
catchError((error) => {
screen.checkPageIsOpen(); // this fails the job if the browser has closed
this.logger.error(error);
+ eventLogger.error(error, Transactions.SCREENSHOTTING);
return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture
}),
takeUntil(unexpectedExit$),
@@ -166,16 +160,8 @@ export class Screenshots {
take(options.urls.length),
toArray(),
mergeMap((results) =>
- // At this point we no longer need the page, close it.
- close().pipe(
- tap(({ metrics }) => {
- if (metrics) {
- transaction?.setLabel('cpu', metrics.cpu, false);
- transaction?.setLabel('memory', metrics.memory, false);
- }
- }),
- map(({ metrics }) => ({ metrics, results }))
- )
+ // At this point we no longer need the page, close it and send out the results
+ close().pipe(map(({ metrics }) => ({ metrics, results })))
)
);
}),
@@ -243,15 +229,28 @@ export class Screenshots {
if (this.systemHasInsufficientMemory()) {
return throwError(() => new errors.InsufficientMemoryAvailableOnCloudError());
}
- const transaction = apm.startTransaction('screenshot-pipeline', 'screenshotting');
- const layout = this.createLayout(transaction, options);
+
+ const eventLogger = new EventLogger(this.logger, this.config);
+ const transactionEnd = eventLogger.startTransaction(Transactions.SCREENSHOTTING);
+
+ const layout = this.createLayout(options);
const captureOptions = this.getCaptureOptions(options);
- return this.captureScreenshots(layout, transaction, captureOptions).pipe(
+ return this.captureScreenshots(eventLogger, layout, captureOptions).pipe(
+ tap(({ results, metrics }) => {
+ transactionEnd({
+ labels: {
+ cpu: metrics?.cpu,
+ memory: metrics?.memory,
+ memory_mb: metrics?.memoryInMegabytes,
+ ...eventLogger.getByteLengthFromCaptureResults(results),
+ },
+ });
+ }),
mergeMap((result) => {
switch (options.format) {
case 'pdf':
- return toPdf(this.logger, this.packageInfo, layout, options, result);
+ return toPdf(eventLogger, this.packageInfo, layout, options, result);
default:
return toPng(result);
}
diff --git a/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts
index a77cfa8c9e8e6..41426e893ce58 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts
@@ -7,26 +7,31 @@
import fs from 'fs';
import { promisify } from 'util';
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { CONTEXT_INJECTCSS } from './constants';
+import { Actions, EventLogger } from './event_logger';
const fsp = { readFile: promisify(fs.readFile) };
export const injectCustomCss = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
layout: Layout
): Promise => {
- const span = apm.startSpan('inject_css', 'correction');
- logger.debug('injecting custom css');
-
const filePath = layout.getCssOverridesPath();
if (!filePath) {
return;
}
+
+ const { kbnLogger } = eventLogger;
+
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'inject CSS into the page',
+ Actions.INJECT_CSS,
+ 'correction'
+ );
+
const buffer = await fsp.readFile(filePath);
try {
await browser.evaluate(
@@ -40,14 +45,15 @@ export const injectCustomCss = async (
args: [buffer.toString()],
},
{ context: CONTEXT_INJECTCSS },
- logger
+ kbnLogger
);
} catch (err) {
- logger.error(err);
+ kbnLogger.error(err);
+ eventLogger.error(err, Actions.INJECT_CSS);
throw new Error(
`An error occurred when trying to update Kibana CSS for reporting. ${err.message}`
);
}
- span?.end();
+ spanEnd();
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts
index d3acc96411dc6..b282cd32bbd80 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts
@@ -5,36 +5,33 @@
* 2.0.
*/
+import { loggingSystemMock } from '@kbn/core/server/mocks';
import { interval, of, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
-import type { Logger } from '@kbn/core/server';
import { createMockBrowserDriver } from '../browsers/mock';
import type { ConfigType } from '../config';
import { createMockLayout } from '../layouts/mock';
+import { EventLogger } from './event_logger';
import { ScreenshotObservableHandler, ScreenshotObservableOptions } from './observable';
describe('ScreenshotObservableHandler', () => {
let browser: ReturnType;
let config: ConfigType;
let layout: ReturnType;
- let logger: jest.Mocked;
+ let eventLogger: EventLogger;
let options: ScreenshotObservableOptions;
beforeEach(async () => {
browser = createMockBrowserDriver();
config = {
capture: {
- timeouts: {
- openUrl: 30000,
- waitForElements: 30000,
- renderComplete: 30000,
- },
+ timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 },
loadDelay: 5000,
zoom: 13,
},
} as ConfigType;
layout = createMockLayout();
- logger = { error: jest.fn() } as unknown as jest.Mocked;
+ eventLogger = new EventLogger(loggingSystemMock.createLogger(), config);
options = {
headers: { testHeader: 'testHeadValue' },
urls: [],
@@ -46,7 +43,7 @@ describe('ScreenshotObservableHandler', () => {
describe('waitUntil', () => {
let screenshots: ScreenshotObservableHandler;
beforeEach(() => {
- screenshots = new ScreenshotObservableHandler(browser, config, logger, layout, options);
+ screenshots = new ScreenshotObservableHandler(browser, config, eventLogger, layout, options);
});
it('catches TimeoutError and references the timeout config in a custom message', async () => {
@@ -79,7 +76,7 @@ describe('ScreenshotObservableHandler', () => {
describe('checkPageIsOpen', () => {
let screenshots: ScreenshotObservableHandler;
beforeEach(() => {
- screenshots = new ScreenshotObservableHandler(browser, config, logger, layout, options);
+ screenshots = new ScreenshotObservableHandler(browser, config, eventLogger, layout, options);
});
it('throws a decorated Error when page is not open', async () => {
diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts
index b19f3f254b2a2..5048d3f0a3be6 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/observable.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts
@@ -5,15 +5,15 @@
* 2.0.
*/
-import type { Transaction } from 'elastic-apm-node';
+import type { Headers } from '@kbn/core/server';
import { defer, forkJoin, Observable, throwError } from 'rxjs';
import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators';
-import type { Headers, Logger } from '@kbn/core/server';
import { errors } from '../../common';
import type { Context, HeadlessChromiumDriver } from '../browsers';
import { DEFAULT_VIEWPORT, getChromiumDisconnectedError } from '../browsers';
-import { durationToNumber as toNumber, ConfigType } from '../config';
+import { ConfigType, durationToNumber as toNumber } from '../config';
import type { Layout } from '../layouts';
+import { Actions, EventLogger } from './event_logger';
import type { ElementsPositionAndAttribute } from './get_element_position_data';
import { getElementPositionAndAttributes } from './get_element_position_data';
import { getNumberOfItems } from './get_number_of_items';
@@ -90,7 +90,9 @@ interface PageSetupResults {
renderErrors?: string[];
}
-const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => {
+const getDefaultElementPosition = (
+ dimensions: { height?: number; width?: number } | null
+): ElementsPositionAndAttribute[] => {
const height = dimensions?.height || DEFAULT_VIEWPORT.height;
const width = dimensions?.width || DEFAULT_VIEWPORT.width;
@@ -118,7 +120,7 @@ export class ScreenshotObservableHandler {
constructor(
private readonly driver: HeadlessChromiumDriver,
private readonly config: ConfigType,
- private readonly logger: Logger,
+ private readonly eventLogger: EventLogger,
private readonly layout: Layout,
private options: ScreenshotObservableOptions
) {}
@@ -154,7 +156,7 @@ export class ScreenshotObservableHandler {
return openUrl(
this.driver,
- this.logger,
+ this.eventLogger,
toNumber(this.config.capture.timeouts.openUrl),
index,
url,
@@ -168,52 +170,70 @@ export class ScreenshotObservableHandler {
const driver = this.driver;
const waitTimeout = toNumber(this.config.capture.timeouts.waitForElements);
- return defer(() => getNumberOfItems(driver, this.logger, waitTimeout, this.layout)).pipe(
+ return defer(() => getNumberOfItems(driver, this.eventLogger, waitTimeout, this.layout)).pipe(
mergeMap(async (itemsCount) => {
// set the viewport to the dimensions from the job, to allow elements to flow into the expected layout
const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort();
// Set the viewport allowing time for the browser to handle reflow and redraw
// before checking for readiness of visualizations.
- await driver.setViewport(viewport, this.logger);
- await waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout);
+ await driver.setViewport(viewport, this.eventLogger.kbnLogger);
+ await waitForVisualizations(driver, this.eventLogger, waitTimeout, itemsCount, this.layout);
}),
this.waitUntil(waitTimeout, 'wait for elements')
);
}
- private completeRender(apmTrans: Transaction | null) {
+ private completeRender() {
const driver = this.driver;
const layout = this.layout;
- const logger = this.logger;
+ const eventLogger = this.eventLogger;
return defer(async () => {
// Waiting till _after_ elements have rendered before injecting our CSS
// allows for them to be displayed properly in many cases
- await injectCustomCss(driver, logger, layout);
+ await injectCustomCss(driver, eventLogger, layout);
- const apmPositionElements = apmTrans?.startSpan('position-elements', 'correction');
- // position panel elements for print layout
- await layout.positionElements?.(driver, logger);
- apmPositionElements?.end();
+ const spanEnd = this.eventLogger.logScreenshottingEvent(
+ 'get positions of visualization elements',
+ Actions.GET_ELEMENT_POSITION_DATA,
+ 'read'
+ );
+ try {
+ // position panel elements for print layout
+ await layout.positionElements?.(driver, eventLogger.kbnLogger);
+ spanEnd();
+ } catch (error) {
+ eventLogger.error(error, Actions.GET_ELEMENT_POSITION_DATA);
+ throw error;
+ }
- await waitForRenderComplete(driver, logger, toNumber(this.config.capture.loadDelay), layout);
+ await waitForRenderComplete(
+ driver,
+ eventLogger,
+ toNumber(this.config.capture.loadDelay),
+ layout
+ );
}).pipe(
mergeMap(() =>
forkJoin({
- timeRange: getTimeRange(driver, logger, layout),
- elementsPositionAndAttributes: getElementPositionAndAttributes(driver, logger, layout),
- renderErrors: getRenderErrors(driver, logger, layout),
+ timeRange: getTimeRange(driver, eventLogger, layout),
+ elementsPositionAndAttributes: getElementPositionAndAttributes(
+ driver,
+ eventLogger,
+ layout
+ ),
+ renderErrors: getRenderErrors(driver, eventLogger, layout),
})
),
this.waitUntil(toNumber(this.config.capture.timeouts.renderComplete), 'render complete')
);
}
- public setupPage(index: number, url: UrlOrUrlWithContext, apmTrans: Transaction | null) {
+ public setupPage(index: number, url: UrlOrUrlWithContext) {
return this.openUrl(index, url).pipe(
switchMapTo(this.waitForElements()),
- switchMapTo(this.completeRender(apmTrans))
+ switchMapTo(this.completeRender())
);
}
@@ -227,7 +247,7 @@ export class ScreenshotObservableHandler {
getDefaultElementPosition(this.layout.getViewport(1));
let screenshots: Screenshot[] = [];
try {
- screenshots = await getScreenshots(this.driver, this.logger, elements);
+ screenshots = await getScreenshots(this.driver, this.eventLogger, elements);
} catch (e) {
throw new errors.FailedToCaptureScreenshot(e.message);
}
diff --git a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts
index c557374ff9876..bdf8c678eb1d2 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/open_url.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts
@@ -5,33 +5,39 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Headers, Logger } from '@kbn/core/server';
-import type { HeadlessChromiumDriver } from '../browsers';
-import type { Context } from '../browsers';
+import type { Headers } from '@kbn/core/server';
+import type { Context, HeadlessChromiumDriver } from '../browsers';
import { DEFAULT_PAGELOAD_SELECTOR } from './constants';
+import { Actions, EventLogger } from './event_logger';
export const openUrl = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
timeout: number,
index: number,
url: string,
context: Context,
headers: Headers
): Promise => {
+ const { kbnLogger } = eventLogger;
+ const spanEnd = eventLogger.logScreenshottingEvent('open url', Actions.OPEN_URL, 'wait');
+
// If we're moving to another page in the app, we'll want to wait for the app to tell us
// it's loaded the next page.
const page = index + 1;
const waitForSelector = page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR;
- const span = apm.startSpan('open_url', 'wait');
try {
- await browser.open(url, { context, headers, waitForSelector, timeout }, logger);
+ await browser.open(url, { context, headers, waitForSelector, timeout }, kbnLogger);
} catch (err) {
- logger.error(err);
- throw new Error(`An error occurred when trying to open the Kibana URL: ${err.message}`);
+ kbnLogger.error(err);
+
+ const newError = new Error(
+ `An error occurred when trying to open the Kibana URL: ${err.message}`
+ );
+ eventLogger.error(newError, Actions.OPEN_URL);
+ throw newError;
}
- span?.end();
+ spanEnd();
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts
similarity index 98%
rename from x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts
rename to x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts
index 6d6dd21347974..0cc40a83723a9 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/semaphore.test.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.test.ts
@@ -6,7 +6,7 @@
*/
import { TestScheduler } from 'rxjs/testing';
-import { Semaphore } from './semaphore';
+import { Semaphore } from '.';
describe('Semaphore', () => {
let testScheduler: TestScheduler;
diff --git a/x-pack/plugins/screenshotting/server/screenshots/semaphore.ts b/x-pack/plugins/screenshotting/server/screenshots/semaphore/index.ts
similarity index 100%
rename from x-pack/plugins/screenshotting/server/screenshots/semaphore.ts
rename to x-pack/plugins/screenshotting/server/screenshots/semaphore/index.ts
diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts
index cee23616faeac..8cf8174be152f 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts
@@ -5,21 +5,22 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { CONTEXT_WAITFORRENDER } from './constants';
+import { Actions, EventLogger } from './event_logger';
export const waitForRenderComplete = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
loadDelay: number,
layout: Layout
) => {
- const span = apm.startSpan('wait_for_render', 'wait');
-
- logger.debug('waiting for rendering to complete');
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'wait for render complete',
+ Actions.WAIT_RENDER,
+ 'wait'
+ );
return await browser
.evaluate(
@@ -66,11 +67,9 @@ export const waitForRenderComplete = async (
args: [layout.selectors.renderComplete, loadDelay],
},
{ context: CONTEXT_WAITFORRENDER },
- logger
+ eventLogger.kbnLogger
)
.then(() => {
- logger.debug('rendering is complete');
-
- span?.end();
+ spanEnd();
});
};
diff --git a/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts
index a7485545cdef0..cf49fbe7dc798 100644
--- a/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts
+++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts
@@ -5,11 +5,10 @@
* 2.0.
*/
-import apm from 'elastic-apm-node';
-import type { Logger } from '@kbn/core/server';
import type { HeadlessChromiumDriver } from '../browsers';
import { Layout } from '../layouts';
import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants';
+import { Actions, EventLogger } from './event_logger';
interface CompletedItemsCountParameters {
context: string;
@@ -37,15 +36,21 @@ const getCompletedItemsCount = ({
*/
export const waitForVisualizations = async (
browser: HeadlessChromiumDriver,
- logger: Logger,
+ eventLogger: EventLogger,
timeout: number,
toEqual: number,
layout: Layout
): Promise => {
- const span = apm.startSpan('wait_for_visualizations', 'wait');
+ const { kbnLogger } = eventLogger;
+ const spanEnd = eventLogger.logScreenshottingEvent(
+ 'waiting for each visualization to complete rendering',
+ Actions.WAIT_VISUALIZATIONS,
+ 'wait'
+ );
+
const { renderComplete: renderCompleteSelector } = layout.selectors;
- logger.debug(`waiting for ${toEqual} rendered elements to be in the DOM`);
+ kbnLogger.debug(`waiting for ${toEqual} rendered elements to be in the DOM`);
try {
await browser.waitFor({
@@ -54,13 +59,15 @@ export const waitForVisualizations = async (
timeout,
});
- logger.debug(`found ${toEqual} rendered elements in the DOM`);
+ kbnLogger.debug(`found ${toEqual} rendered elements in the DOM`);
} catch (err) {
- logger.error(err);
- throw new Error(
+ kbnLogger.error(err);
+ const newError = new Error(
`An error occurred when trying to wait for ${toEqual} visualizations to finish rendering. ${err.message}`
);
+ eventLogger.error(newError, Actions.WAIT_VISUALIZATIONS);
+ throw newError;
}
- span?.end();
+ spanEnd();
};
diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts
index caeeaa0c17bee..cb03788aa17ba 100644
--- a/x-pack/plugins/security_solution/common/types/timeline/index.ts
+++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts
@@ -318,7 +318,7 @@ export enum TimelineId {
usersPageExternalAlerts = 'users-page-external-alerts',
hostsPageEvents = 'hosts-page-events',
hostsPageExternalAlerts = 'hosts-page-external-alerts',
- hostsPageSessions = 'hosts-page-sessions',
+ hostsPageSessions = 'hosts-page-sessions-v2', // the v2 is to cache bust localstorage settings as default columns were reworked.
detectionsRulesDetailsPage = 'detections-rules-details-page',
detectionsPage = 'detections-page',
networkPageExternalAlerts = 'network-page-external-alerts',
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap
index 32268e2f21e7f..9d32d2c23b18b 100644
--- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/__snapshots__/index.test.tsx.snap
@@ -70,34 +70,28 @@ exports[`SessionsView renders correctly against snapshot 1`] = `
- hosts-page-sessions
+ hosts-page-sessions-v2
- process.start
+ Started
- process.end
+ Executable
- process.executable
+ User
- user.name
+ Interactive
- process.interactive
+ Hostname
- process.pid
+ Type
- host.hostname
-
-
- process.entry_leader.entry_meta.type
-
-
- process.entry_leader.entry_meta.source.ip
+ Source IP
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx
deleted file mode 100644
index 088935b32ce34..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/cell_renderer.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
-import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
-import { getEmptyValue } from '../empty_value';
-import { MAPPED_PROCESS_END_COLUMN } from './default_headers';
-
-const hasEcsDataEndEventAction = (ecsData: CellValueElementProps['ecsData']) => {
- return ecsData?.event?.action?.includes('end');
-};
-
-export const CellRenderer: React.FC = (props: CellValueElementProps) => {
- // We only want to render process.end for event.actions of type 'end'
- if (props.columnId === MAPPED_PROCESS_END_COLUMN && !hasEcsDataEndEventAction(props.ecsData)) {
- return <>{getEmptyValue()}>;
- }
-
- return ;
-};
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts
index d73ab1b690f61..4c045e358e1d6 100644
--- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts
+++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/default_headers.ts
@@ -10,50 +10,52 @@ import { defaultColumnHeaderType } from '../../../timelines/components/timeline/
import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants';
import { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
-
-// Using @timestamp as an way of getting the end time of the process. (Currently endpoint doesn't populate process.end)
-// @timestamp of an event.action with value of "end" is what we consider that to be the end time of the process
-// Current action are: 'start', 'exec', 'end', so we might have up to three events per process.
-export const MAPPED_PROCESS_END_COLUMN = '@timestamp';
+import {
+ COLUMN_SESSION_START,
+ COLUMN_EXECUTABLE,
+ COLUMN_ENTRY_USER,
+ COLUMN_INTERACTIVE,
+ COLUMN_HOST_NAME,
+ COLUMN_ENTRY_TYPE,
+ COLUMN_ENTRY_IP,
+} from './translations';
export const sessionsHeaders: ColumnHeaderOptions[] = [
{
columnHeaderType: defaultColumnHeaderType,
- id: 'process.start',
+ id: 'process.entry_leader.start',
initialWidth: DEFAULT_DATE_COLUMN_MIN_WIDTH,
+ display: COLUMN_SESSION_START,
},
{
columnHeaderType: defaultColumnHeaderType,
- id: MAPPED_PROCESS_END_COLUMN,
- display: 'process.end',
+ id: 'process.entry_leader.executable',
+ display: COLUMN_EXECUTABLE,
},
{
columnHeaderType: defaultColumnHeaderType,
- id: 'process.executable',
+ id: 'process.entry_leader.user.name',
+ display: COLUMN_ENTRY_USER,
},
{
columnHeaderType: defaultColumnHeaderType,
- id: 'user.name',
- },
- {
- columnHeaderType: defaultColumnHeaderType,
- id: 'process.interactive',
- },
- {
- columnHeaderType: defaultColumnHeaderType,
- id: 'process.pid',
+ id: 'process.entry_leader.interactive',
+ display: COLUMN_INTERACTIVE,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'host.hostname',
+ display: COLUMN_HOST_NAME,
},
{
columnHeaderType: defaultColumnHeaderType,
id: 'process.entry_leader.entry_meta.type',
+ display: COLUMN_ENTRY_TYPE,
},
{
- columnHeaderType: defaultColumnHeaderType,
id: 'process.entry_leader.entry_meta.source.ip',
+ columnHeaderType: defaultColumnHeaderType,
+ display: COLUMN_ENTRY_IP,
},
];
@@ -62,4 +64,11 @@ export const sessionsDefaultModel: SubsetTimelineModel = {
columns: sessionsHeaders,
defaultColumns: sessionsHeaders,
excludedRowRendererIds: Object.values(RowRendererId),
+ sort: [
+ {
+ columnId: 'process.entry_leader.start',
+ columnType: 'date',
+ sortDirection: 'desc',
+ },
+ ],
};
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx
index 043a2aa378427..5280f298ba99e 100644
--- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.test.tsx
@@ -109,10 +109,11 @@ describe('SessionsView', () => {
expect(wrapper.getByTestId(`${TEST_PREFIX}:startDate`)).toHaveTextContent(startDate);
expect(wrapper.getByTestId(`${TEST_PREFIX}:endDate`)).toHaveTextContent(endDate);
expect(wrapper.getByTestId(`${TEST_PREFIX}:timelineId`)).toHaveTextContent(
- 'hosts-page-sessions'
+ 'hosts-page-sessions-v2'
);
});
});
+
it('passes in the right filters to TGrid', async () => {
render(
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx
index 6834553a5eee8..4d89b969e5c17 100644
--- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/index.tsx
@@ -12,7 +12,7 @@ import { ESBoolQuery } from '../../../../common/typed_json';
import { StatefulEventsViewer } from '../events_viewer';
import { sessionsDefaultModel } from './default_headers';
import { defaultRowRenderers } from '../../../timelines/components/timeline/body/renderers';
-import { CellRenderer } from './cell_renderer';
+import { DefaultCellRenderer } from '../../../timelines/components/timeline/cell_rendering/default_cell_renderer';
import * as i18n from './translations';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { getDefaultControlColumn } from '../../../timelines/components/timeline/body/control_columns';
@@ -24,15 +24,8 @@ export const defaultSessionsFilter: Required> = {
bool: {
filter: [
{
- bool: {
- should: [
- {
- match: {
- 'process.entry_leader.same_as_process': true,
- },
- },
- ],
- minimum_should_match: 1,
+ exists: {
+ field: 'process.entry_leader.entity_id', // to exclude any records which have no entry_leader.entity_id
},
},
],
@@ -41,10 +34,10 @@ export const defaultSessionsFilter: Required> = {
meta: {
alias: null,
disabled: false,
- key: 'process.entry_leader.same_as_process',
+ key: 'process.entry_leader.entity_id',
negate: false,
params: {},
- type: 'boolean',
+ type: 'string',
},
};
@@ -95,7 +88,7 @@ const SessionsViewComponent: React.FC = ({
entityType={entityType}
id={timelineId}
leadingControlColumns={leadingControlColumns}
- renderCellValue={CellRenderer}
+ renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
scopeId={SourcererScopeName.default}
start={startDate}
diff --git a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts
index 606ae2b46fc6a..ea35892f3a2f9 100644
--- a/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/sessions_viewer/translations.ts
@@ -20,3 +20,52 @@ export const SINGLE_COUNT_OF_SESSIONS = i18n.translate(
defaultMessage: 'session',
}
);
+
+export const COLUMN_SESSION_START = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnSessionStart',
+ {
+ defaultMessage: 'Started',
+ }
+);
+
+export const COLUMN_EXECUTABLE = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnExecutable',
+ {
+ defaultMessage: 'Executable',
+ }
+);
+
+export const COLUMN_ENTRY_USER = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnEntryUser',
+ {
+ defaultMessage: 'User',
+ }
+);
+
+export const COLUMN_INTERACTIVE = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnInteractive',
+ {
+ defaultMessage: 'Interactive',
+ }
+);
+
+export const COLUMN_HOST_NAME = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnHostName',
+ {
+ defaultMessage: 'Hostname',
+ }
+);
+
+export const COLUMN_ENTRY_TYPE = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnEntryType',
+ {
+ defaultMessage: 'Type',
+ }
+);
+
+export const COLUMN_ENTRY_IP = i18n.translate(
+ 'xpack.securitySolution.sessionsView.columnEntrySourceIp',
+ {
+ defaultMessage: 'Source IP',
+ }
+);
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx
index 3137862025f15..0439eff39d88c 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.test.tsx
@@ -6,23 +6,10 @@
*/
import React from 'react';
-import { fireEvent, waitFor } from '@testing-library/react';
import { render } from '../../../utils/testing/rtl_helpers';
import { ActionMenuContent } from './action_menu_content';
describe('ActionMenuContent', () => {
- it('renders alerts dropdown', async () => {
- const { getByLabelText, getByText } = render();
-
- const alertsDropdown = getByLabelText('Open alerts and rules context menu');
- fireEvent.click(alertsDropdown);
-
- await waitFor(() => {
- expect(getByText('Create rule'));
- expect(getByText('Manage rules'));
- });
- });
-
it('renders settings link', () => {
const { getByRole, getByText } = render();
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx
index 6d3d83146a42c..aaf41dc46bcaf 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/header/action_menu_content.tsx
@@ -14,12 +14,9 @@ import { createExploratoryViewUrl } from '@kbn/observability-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useSyntheticsSettingsContext } from '../../../contexts';
import { useGetUrlParams } from '../../../hooks';
-import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers/toggle_alert_flyout_button';
import { MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../../../common/constants';
import { stringifyUrlParams } from '../../../utils/url_params';
import { InspectorHeaderLink } from './inspector_header_link';
-// import { monitorStatusSelector } from '../../../state/selectors';
-// import { ManageMonitorsBtn } from './manage_monitors_btn';
const ADD_DATA_LABEL = i18n.translate('xpack.synthetics.addDataButtonLabel', {
defaultMessage: 'Add data',
@@ -93,8 +90,6 @@ export function ActionMenuContent(): React.ReactElement {
/>
-
-
{ANALYZE_MESSAGE}
}>
{
+jest.mock('../../../../../hooks/use_breakpoints', () => {
const down = jest.fn().mockReturnValue(false);
return {
useBreakpoints: () => ({ down }),
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx
index 0e6c5565b842e..44b38236fc2a2 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx
@@ -10,7 +10,6 @@ import styled from 'styled-components';
import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useInspectorContext } from '@kbn/observability-plugin/public';
-import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE } from '../../../../../../common/constants';
import { ClientPluginsStart } from '../../../../../plugin';
import { useNoDataConfig } from '../../../hooks/use_no_data_config';
import { EmptyStateLoading } from '../../overview/empty_state/empty_state_loading';
@@ -65,9 +64,7 @@ export const SyntheticsPageTemplateComponent: React.FC;
}
- const isMainRoute = path === OVERVIEW_ROUTE || path === CERTIFICATES_ROUTE;
-
- const showLoading = loading && isMainRoute && !data;
+ const showLoading = loading && !data;
return (
<>
@@ -75,7 +72,7 @@ export const SyntheticsPageTemplateComponent: React.FC
{showLoading && }
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx
deleted file mode 100644
index 7271e8cd2e998..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import {
- selectAlertFlyoutVisibility,
- selectAlertFlyoutType,
- setAlertFlyoutVisible,
-} from '../../../../state';
-import { SyntheticsAlertsFlyoutWrapperComponent } from '../synthetics_alerts_flyout_wrapper';
-
-export const SyntheticsAlertsFlyoutWrapper: React.FC = () => {
- const dispatch = useDispatch();
- const setAddFlyoutVisibility = (value: React.SetStateAction) =>
- // @ts-ignore the value here is a boolean, and it works with the action creator function
- dispatch(setAlertFlyoutVisible(value));
-
- const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
- const alertTypeId = useSelector(selectAlertFlyoutType);
-
- return (
-
- );
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx
deleted file mode 100644
index 2fea38d99d094..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/alerts_containers/toggle_alert_flyout_button.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import { useDispatch } from 'react-redux';
-import { setAlertFlyoutType, setAlertFlyoutVisible } from '../../../../state';
-import {
- ToggleAlertFlyoutButtonComponent,
- ToggleAlertFlyoutButtonProps,
-} from '../toggle_alert_flyout_button';
-
-export const ToggleAlertFlyoutButton: React.FC = (props) => {
- const dispatch = useDispatch();
- return (
- {
- if (typeof value === 'string') {
- dispatch(setAlertFlyoutType(value));
- dispatch(setAlertFlyoutVisible(true));
- } else {
- dispatch(setAlertFlyoutVisible(value));
- }
- }}
- />
- );
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx
deleted file mode 100644
index 33c76176787cf..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/synthetics_alerts_flyout_wrapper.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { useCallback, useMemo } from 'react';
-import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
-
-interface Props {
- alertFlyoutVisible: boolean;
- alertTypeId?: string;
- setAlertFlyoutVisibility: React.Dispatch>;
-}
-
-interface KibanaDeps {
- triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
-}
-
-export const SyntheticsAlertsFlyoutWrapperComponent = ({
- alertFlyoutVisible,
- alertTypeId,
- setAlertFlyoutVisibility,
-}: Props) => {
- const { triggersActionsUi } = useKibana().services;
- const onCloseAlertFlyout = useCallback(
- () => setAlertFlyoutVisibility(false),
- [setAlertFlyoutVisibility]
- );
- const AddAlertFlyout = useMemo(
- () =>
- triggersActionsUi.getAddAlertFlyout({
- consumer: 'synthetics',
- onClose: onCloseAlertFlyout,
- ruleTypeId: alertTypeId,
- canChangeTrigger: !alertTypeId,
- }),
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [onCloseAlertFlyout, alertTypeId]
- );
-
- return <>{alertFlyoutVisible && AddAlertFlyout}>;
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx
deleted file mode 100644
index b185d3897d243..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.test.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React from 'react';
-import userEvent from '@testing-library/user-event';
-import { render, forNearestButton, makeSyntheticsPermissionsCore } from '../../../utils/testing';
-import { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button';
-import { ToggleFlyoutTranslations } from './translations';
-
-describe('ToggleAlertFlyoutButtonComponent', () => {
- describe('when users have write access to synthetics', () => {
- it('enables the button to create a rule', () => {
- const { getByText } = render(
- ,
- { core: makeSyntheticsPermissionsCore({ save: true }) }
- );
- userEvent.click(getByText('Alerts and rules'));
- expect(
- forNearestButton(getByText)(ToggleFlyoutTranslations.openAlertContextPanelLabel)
- ).toBeEnabled();
- });
-
- it("does not contain a tooltip explaining why the user can't create alerts", async () => {
- const { getByText, findByText } = render(
- ,
- { core: makeSyntheticsPermissionsCore({ save: true }) }
- );
- userEvent.click(getByText('Alerts and rules'));
- userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel));
- await expect(() =>
- findByText('You need read-write access to Synthetics to create alerts in this app.')
- ).rejects.toEqual(expect.anything());
- });
- });
-
- describe("when users don't have write access to uptime", () => {
- it('disables the button to create a rule', () => {
- const { getByText } = render(
- ,
- { core: makeSyntheticsPermissionsCore({ save: false }) }
- );
- userEvent.click(getByText('Alerts and rules'));
- expect(
- forNearestButton(getByText)(ToggleFlyoutTranslations.openAlertContextPanelLabel)
- ).toBeDisabled();
- });
-
- it("contains a tooltip explaining why users can't create rules", async () => {
- const { getByText, findByText } = render(
- ,
- { core: makeSyntheticsPermissionsCore({ save: false }) }
- );
- userEvent.click(getByText('Alerts and rules'));
- userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel));
- expect(
- await findByText('You need read-write access to Uptime to create alerts in this app.')
- ).toBeInTheDocument();
- });
- });
-});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx
deleted file mode 100644
index f29fe0941ee82..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/toggle_alert_flyout_button.tsx
+++ /dev/null
@@ -1,164 +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 {
- EuiHeaderLink,
- EuiContextMenu,
- EuiContextMenuPanelDescriptor,
- EuiContextMenuPanelItemDescriptor,
- EuiLink,
- EuiPopover,
-} from '@elastic/eui';
-import React, { useState } from 'react';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n-react';
-import { useKibana } from '@kbn/kibana-react-plugin/public';
-import { CLIENT_ALERT_TYPES } from '../../../../../../common/constants/alerts';
-import { ClientPluginsStart } from '../../../../../plugin';
-
-import { ToggleFlyoutTranslations } from './translations';
-
-interface ComponentProps {
- setAlertFlyoutVisible: (value: boolean | string) => void;
-}
-
-export interface ToggleAlertFlyoutButtonProps {
- alertOptions?: string[];
-}
-
-type Props = ComponentProps & ToggleAlertFlyoutButtonProps;
-
-const ALERT_CONTEXT_MAIN_PANEL_ID = 0;
-const ALERT_CONTEXT_SELECT_TYPE_PANEL_ID = 1;
-
-const noWritePermissionsTooltipContent = i18n.translate(
- 'xpack.synthetics.alertDropdown.noWritePermissions',
- {
- defaultMessage: 'You need read-write access to Uptime to create alerts in this app.',
- }
-);
-
-export const ToggleAlertFlyoutButtonComponent: React.FC = ({
- alertOptions,
- setAlertFlyoutVisible,
-}) => {
- const [isOpen, setIsOpen] = useState(false);
- const kibana = useKibana();
- const {
- services: { observability },
- } = useKibana();
- const manageRulesUrl = observability.useRulesLink();
- const hasUptimeWrite = kibana.services.application?.capabilities.uptime?.save ?? false;
-
- const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
- 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
- 'data-test-subj': 'xpack.synthetics.toggleAlertFlyout',
- name: ToggleFlyoutTranslations.toggleMonitorStatusContent,
- onClick: () => {
- setAlertFlyoutVisible(CLIENT_ALERT_TYPES.MONITOR_STATUS);
- setIsOpen(false);
- },
- };
-
- const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
- 'aria-label': ToggleFlyoutTranslations.toggleTlsAriaLabel,
- 'data-test-subj': 'xpack.synthetics.toggleTlsAlertFlyout',
- name: ToggleFlyoutTranslations.toggleTlsContent,
- onClick: () => {
- setAlertFlyoutVisible(CLIENT_ALERT_TYPES.TLS);
- setIsOpen(false);
- },
- };
-
- const managementContextItem: EuiContextMenuPanelItemDescriptor = {
- 'aria-label': ToggleFlyoutTranslations.navigateToAlertingUIAriaLabel,
- 'data-test-subj': 'xpack.synthetics.navigateToAlertingUi',
- name: (
-
-
-
- ),
- icon: 'tableOfContents',
- };
-
- let selectionItems: EuiContextMenuPanelItemDescriptor[] = [];
- if (!alertOptions) {
- selectionItems = [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem];
- } else {
- alertOptions.forEach((option) => {
- if (option === CLIENT_ALERT_TYPES.MONITOR_STATUS)
- selectionItems.push(monitorStatusAlertContextMenuItem);
- else if (option === CLIENT_ALERT_TYPES.TLS) selectionItems.push(tlsAlertContextMenuItem);
- });
- }
-
- if (selectionItems.length === 1) {
- selectionItems[0].icon = 'bell';
- }
-
- let panels: EuiContextMenuPanelDescriptor[];
- if (selectionItems.length === 1) {
- panels = [
- {
- id: ALERT_CONTEXT_MAIN_PANEL_ID,
- items: [...selectionItems, managementContextItem],
- },
- ];
- } else {
- panels = [
- {
- id: ALERT_CONTEXT_MAIN_PANEL_ID,
- items: [
- {
- 'aria-label': ToggleFlyoutTranslations.openAlertContextPanelAriaLabel,
- 'data-test-subj': 'xpack.synthetics.openAlertContextPanel',
- name: ToggleFlyoutTranslations.openAlertContextPanelLabel,
- icon: 'bell',
- panel: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
- toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
- disabled: !hasUptimeWrite,
- },
- managementContextItem,
- ],
- },
- {
- id: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
- title: ToggleFlyoutTranslations.toggleAlertFlyoutButtonLabel,
- items: selectionItems,
- },
- ];
- }
-
- return (
- setIsOpen(!isOpen)}
- >
-
-
- }
- closePopover={() => setIsOpen(false)}
- isOpen={isOpen}
- ownFocus
- panelPaddingSize="none"
- >
-
-
- );
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts
deleted file mode 100644
index 0580528b6b38c..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/alerts/translations.ts
+++ /dev/null
@@ -1,345 +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 { i18n } from '@kbn/i18n';
-
-export const SECONDS_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.secondsOption.ariaLabel',
- {
- defaultMessage: '"Seconds" time range select item',
- }
-);
-
-export const SECONDS = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.timerangeOption.seconds',
- {
- defaultMessage: 'seconds',
- }
-);
-
-export const MINUTES_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.minutesOption.ariaLabel',
- {
- defaultMessage: '"Minutes" time range select item',
- }
-);
-
-export const MINUTES = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.timerangeOption.minutes',
- {
- defaultMessage: 'minutes',
- }
-);
-
-export const HOURS_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.hoursOption.ariaLabel',
- {
- defaultMessage: '"Hours" time range select item',
- }
-);
-
-export const HOURS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.hours', {
- defaultMessage: 'hours',
-});
-
-export const DAYS_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.daysOption.ariaLabel',
- {
- defaultMessage: '"Days" time range select item',
- }
-);
-
-export const DAYS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.days', {
- defaultMessage: 'days',
-});
-
-export const WEEKS_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.weeksOption.ariaLabel',
- {
- defaultMessage: '"Weeks" time range select item',
- }
-);
-
-export const WEEKS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.weeks', {
- defaultMessage: 'weeks',
-});
-
-export const MONTHS_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.monthsOption.ariaLabel',
- {
- defaultMessage: '"Months" time range select item',
- }
-);
-
-export const MONTHS = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.timerangeOption.months',
- {
- defaultMessage: 'months',
- }
-);
-
-export const YEARS_TIME_RANGE = i18n.translate(
- 'xpack.synthetics.alerts.timerangeUnitSelectable.yearsOption.ariaLabel',
- {
- defaultMessage: '"Years" time range select item',
- }
-);
-
-export const YEARS = i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeOption.years', {
- defaultMessage: 'years',
-});
-
-export const ALERT_KUERY_BAR_ARIA = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.filterBar.ariaLabel',
- {
- defaultMessage: 'Input that allows filtering criteria for the monitor status alert',
- }
-);
-
-export const OPEN_THE_POPOVER_DOWN_COUNT = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.numTimesExpression.ariaLabel',
- {
- defaultMessage: 'Open the popover for down count input',
- }
-);
-
-export const ENTER_NUMBER_OF_DOWN_COUNTS = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.numTimesField.ariaLabel',
- {
- defaultMessage: 'Enter number of down counts required to trigger the alert',
- }
-);
-
-export const MATCHING_MONITORS_DOWN = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.numTimesExpression.matchingMonitors.description',
- {
- defaultMessage: 'matching monitors are down >',
- }
-);
-
-export const ANY_MONITOR_DOWN = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.numTimesExpression.anyMonitors.description',
- {
- defaultMessage: 'any monitor is down >',
- }
-);
-
-export const OPEN_THE_POPOVER_TIME_RANGE_VALUE = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.timerangeValueExpression.ariaLabel',
- {
- defaultMessage: 'Open the popover for time range value field',
- }
-);
-
-export const ENTER_NUMBER_OF_TIME_UNITS = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.timerangeValueField.ariaLabel',
- {
- defaultMessage: `Enter the number of time units for the alert's range`,
- }
-);
-
-export const ENTER_NUMBER_OF_TIME_UNITS_DESCRIPTION = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.timerangeValueField.expression',
- {
- defaultMessage: 'within',
- }
-);
-
-export const ENTER_NUMBER_OF_TIME_UNITS_VALUE = (value: number) =>
- i18n.translate('xpack.synthetics.alerts.monitorStatus.timerangeValueField.value', {
- defaultMessage: 'last {value}',
- values: { value },
- });
-
-export const ENTER_AVAILABILITY_RANGE_ENABLED = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.isEnabledCheckbox.label',
- {
- defaultMessage: 'Availability',
- }
-);
-
-export const ENTER_AVAILABILITY_RANGE_POPOVER_ARIA_LABEL = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.timerangeValueField.popover.ariaLabel',
- {
- defaultMessage: 'Specify availability tracking time range',
- }
-);
-
-export const ENTER_AVAILABILITY_RANGE_UNITS_ARIA_LABEL = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.timerangeValueField.ariaLabel',
- {
- defaultMessage: `Enter the number of units for the alert's availability check.`,
- }
-);
-
-export const ENTER_AVAILABILITY_RANGE_UNITS_DESCRIPTION = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.timerangeValueField.expression',
- {
- defaultMessage: 'within the last',
- }
-);
-
-export const ENTER_AVAILABILITY_THRESHOLD_ARIA_LABEL = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.threshold.ariaLabel',
- {
- defaultMessage: 'Specify availability thresholds for this alert',
- }
-);
-
-export const ENTER_AVAILABILITY_THRESHOLD_INPUT_ARIA_LABEL = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.threshold.input.ariaLabel',
- {
- defaultMessage: 'Input an availability threshold to check for this alert',
- }
-);
-
-export const ENTER_AVAILABILITY_THRESHOLD_DESCRIPTION = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.threshold.description',
- {
- defaultMessage: 'matching monitors are up in',
- description:
- 'This fragment explains that an alert will fire for monitors matching user-specified criteria',
- }
-);
-
-export const ENTER_ANY_AVAILABILITY_THRESHOLD_DESCRIPTION = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.threshold.anyMonitorDescription',
- {
- defaultMessage: 'any monitor is up in',
- description:
- 'This fragment explains that an alert will fire for monitors matching user-specified criteria',
- }
-);
-
-export const ENTER_AVAILABILITY_THRESHOLD_VALUE = (value: string) =>
- i18n.translate('xpack.synthetics.alerts.monitorStatus.availability.threshold.value', {
- defaultMessage: '< {value}% of checks',
- description:
- 'This fragment specifies criteria that will cause an alert to fire for uptime monitors',
- values: { value },
- });
-
-export const ENTER_AVAILABILITY_RANGE_SELECT_ARIA = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.unit.selectable',
- {
- defaultMessage: 'Use this select to set the availability range units for this alert',
- }
-);
-
-export const ENTER_AVAILABILITY_RANGE_SELECT_HEADLINE = i18n.translate(
- 'xpack.synthetics.alerts.monitorStatus.availability.unit.headline',
- {
- defaultMessage: 'Select time range unit',
- }
-);
-
-export const ADD_FILTER = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter', {
- defaultMessage: `Add filter`,
-});
-
-export const LOCATION = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.location', {
- defaultMessage: `Location`,
-});
-
-export const TAG = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.tag', {
- defaultMessage: `Tag`,
-});
-
-export const PORT = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.port', {
- defaultMessage: `Port`,
-});
-
-export const TYPE = i18n.translate('xpack.synthetics.alerts.monitorStatus.addFilter.type', {
- defaultMessage: `Type`,
-});
-
-export const TlsTranslations = {
- criteriaAriaLabel: i18n.translate('xpack.synthetics.alerts.tls.criteriaExpression.ariaLabel', {
- defaultMessage:
- 'An expression displaying the criteria for monitor that are watched by this alert',
- }),
- criteriaDescription: i18n.translate(
- 'xpack.synthetics.alerts.tls.criteriaExpression.description',
- {
- defaultMessage: 'when',
- description:
- 'The context of this `when` is in the conditional sense, like "when there are three cookies, eat them all".',
- }
- ),
- criteriaValue: i18n.translate('xpack.synthetics.alerts.tls.criteriaExpression.value', {
- defaultMessage: 'any monitor',
- }),
- expirationAriaLabel: i18n.translate(
- 'xpack.synthetics.alerts.tls.expirationExpression.ariaLabel',
- {
- defaultMessage:
- 'An expression displaying the threshold that will trigger the TLS alert for certificate expiration',
- }
- ),
- expirationDescription: i18n.translate(
- 'xpack.synthetics.alerts.tls.expirationExpression.description',
- {
- defaultMessage: 'has a certificate expiring within',
- }
- ),
- expirationValue: (value?: number) =>
- i18n.translate('xpack.synthetics.alerts.tls.expirationExpression.value', {
- defaultMessage: '{value} days',
- values: { value },
- }),
- ageAriaLabel: i18n.translate('xpack.synthetics.alerts.tls.ageExpression.ariaLabel', {
- defaultMessage:
- 'An expressing displaying the threshold that will trigger the TLS alert for old certificates',
- }),
- ageDescription: i18n.translate('xpack.synthetics.alerts.tls.ageExpression.description', {
- defaultMessage: 'or older than',
- }),
- ageValue: (value?: number) =>
- i18n.translate('xpack.synthetics.alerts.tls.ageExpression.value', {
- defaultMessage: '{value} days',
- values: { value },
- }),
-};
-
-export const ToggleFlyoutTranslations = {
- toggleButtonAriaLabel: i18n.translate('xpack.synthetics.alertsPopover.toggleButton.ariaLabel', {
- defaultMessage: 'Open alerts and rules context menu',
- }),
- openAlertContextPanelAriaLabel: i18n.translate(
- 'xpack.synthetics.openAlertContextPanel.ariaLabel',
- {
- defaultMessage: 'Open the rule context panel so you can choose a rule type',
- }
- ),
- openAlertContextPanelLabel: i18n.translate('xpack.synthetics.openAlertContextPanel.label', {
- defaultMessage: 'Create rule',
- }),
- toggleTlsAriaLabel: i18n.translate('xpack.synthetics.toggleTlsAlertButton.ariaLabel', {
- defaultMessage: 'Open TLS rule flyout',
- }),
- toggleTlsContent: i18n.translate('xpack.synthetics.toggleTlsAlertButton.content', {
- defaultMessage: 'TLS rule',
- }),
- toggleMonitorStatusAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.ariaLabel', {
- defaultMessage: 'Open add rule flyout',
- }),
- toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', {
- defaultMessage: 'Monitor status rule',
- }),
- navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.navigateToAlertingUi', {
- defaultMessage: 'Leave Uptime and go to Alerting Management page',
- }),
- navigateToAlertingButtonContent: i18n.translate(
- 'xpack.synthetics.navigateToAlertingButton.content',
- {
- defaultMessage: 'Manage rules',
- }
- ),
- toggleAlertFlyoutButtonLabel: i18n.translate('xpack.synthetics.alerts.createRulesPanel.title', {
- defaultMessage: 'Create rules',
- }),
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.ts
deleted file mode 100644
index e1cf5e20e14c3..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/filter_group/labels.ts
+++ /dev/null
@@ -1,24 +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 { i18n } from '@kbn/i18n';
-
-export const filterLabels = {
- LOCATION: i18n.translate('xpack.synthetics.filterBar.options.location.name', {
- defaultMessage: 'Location',
- }),
-
- PORT: i18n.translate('xpack.synthetics.filterBar.options.portLabel', { defaultMessage: 'Port' }),
-
- SCHEME: i18n.translate('xpack.synthetics.filterBar.options.schemeLabel', {
- defaultMessage: 'Scheme',
- }),
-
- TAG: i18n.translate('xpack.synthetics.filterBar.options.tagsLabel', {
- defaultMessage: 'Tag',
- }),
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts
index a7df47d7a0f71..15079dc68823b 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts
@@ -6,9 +6,8 @@
*/
export * from './use_url_params';
-export * from './use_filter_update';
export * from './use_breadcrumbs';
export * from './use_telemetry';
-export * from './use_breakpoints';
+export * from '../../../hooks/use_breakpoints';
export * from './use_service_allowed';
export * from './use_no_data_config';
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts
deleted file mode 100644
index da3a25a5fc9df..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.test.ts
+++ /dev/null
@@ -1,32 +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 { addUpdatedField } from './use_filter_update';
-
-describe('useFilterUpdate', () => {
- describe('addUpdatedField', () => {
- it('conditionally adds fields if they are new', () => {
- const testVal = {};
- addUpdatedField('a val', 'newField', 'a new val', testVal);
- expect(testVal).toEqual({
- newField: 'a new val',
- });
- });
-
- it('will add a field if the value is the same but not the default', () => {
- const testVal = {};
- addUpdatedField('a val', 'newField', 'a val', testVal);
- expect(testVal).toEqual({ newField: 'a val' });
- });
-
- it(`won't add a field if the current value is empty`, () => {
- const testVal = {};
- addUpdatedField('', 'newField', '', testVal);
- expect(testVal).toEqual({});
- });
- });
-});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts
deleted file mode 100644
index 5578230ab2cf0..0000000000000
--- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_filter_update.ts
+++ /dev/null
@@ -1,76 +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 { useEffect } from 'react';
-import { useUrlParams } from './use_url_params';
-
-export const parseFiltersMap = (currentFilters: string): Map => {
- try {
- return new Map(JSON.parse(currentFilters));
- } catch {
- return new Map();
- }
-};
-
-const getUpdateFilters = (
- filterKueries: Map,
- fieldName: string,
- values?: string[]
-): string => {
- // add new term to filter map, toggle it off if already present
- const updatedFilterMap = new Map(filterKueries);
- updatedFilterMap.set(fieldName, values ?? []);
- updatedFilterMap.forEach((value, key) => {
- if (typeof value !== 'undefined' && value.length === 0) {
- updatedFilterMap.delete(key);
- }
- });
-
- // store the new set of filters
- const persistedFilters = Array.from(updatedFilterMap);
- return persistedFilters.length === 0 ? '' : JSON.stringify(persistedFilters);
-};
-
-export function addUpdatedField(
- current: string,
- key: string,
- updated: string,
- objToUpdate: { [key: string]: string }
-): void {
- if (current !== updated || current !== '') {
- objToUpdate[key] = updated;
- }
-}
-
-export const useFilterUpdate = (
- fieldName: string,
- values: string[],
- notValues: string[],
- shouldUpdateUrl: boolean = true
-) => {
- const [getUrlParams, updateUrl] = useUrlParams();
-
- const { filters, excludedFilters } = getUrlParams();
-
- useEffect(() => {
- const currentFiltersMap: Map = parseFiltersMap(filters);
- const currentExclusionsMap: Map = parseFiltersMap(excludedFilters);
- const newFiltersString = getUpdateFilters(currentFiltersMap, fieldName, values);
- const newExclusionsString = getUpdateFilters(currentExclusionsMap, fieldName, notValues);
-
- const update: { [key: string]: string } = {};
-
- addUpdatedField(filters, 'filters', newFiltersString, update);
- addUpdatedField(excludedFilters, 'excludedFilters', newExclusionsString, update);
-
- if (shouldUpdateUrl && Object.keys(update).length > 0) {
- // reset pagination whenever filters change
- updateUrl({ ...update, pagination: '' });
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fieldName, values, notValues]);
-};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts
index f97e4c4b2be09..64ecabaff5d5a 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_telemetry.ts
@@ -7,7 +7,7 @@
import { useEffect } from 'react';
import { useGetUrlParams } from './use_url_params';
-import { apiService } from '../utils/api_service';
+import { apiService } from '../../../utils/api_service';
// import { API_URLS } from '../../../common/constants';
export enum SyntheticsPage {
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx
index d20c390c84b59..7f04b3992885b 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx
@@ -23,7 +23,7 @@ import {
OVERVIEW_ROUTE,
} from '../../../common/constants';
import { MonitorManagementPage } from './components/monitor_management/monitor_management_page';
-import { apiService } from './utils/api_service';
+import { apiService } from '../../utils/api_service';
import { SyntheticsPage, useSyntheticsTelemetry } from './hooks/use_telemetry';
type RouteProps = {
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts
index f2d5e326ba2ab..ba6ded899f9c4 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index_status/api.ts
@@ -7,7 +7,7 @@
import { API_URLS } from '../../../../../common/constants';
import { StatesIndexStatus, StatesIndexStatusType } from '../../../../../common/runtime_types';
-import { apiService } from '../../utils/api_service';
+import { apiService } from '../../../../utils/api_service';
export const fetchIndexStatus = async (): Promise => {
return await apiService.get(API_URLS.INDEX_STATUS, undefined, StatesIndexStatusType);
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx
index 614f77ddff5d7..07fb3604abd42 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/synthetics_app.tsx
@@ -17,7 +17,6 @@ import {
} from '@kbn/kibana-react-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { InspectorContextProvider } from '@kbn/observability-plugin/public';
-import { SyntheticsAlertsFlyoutWrapper } from './components/overview/alerts/alerts_containers/synthetics_alerts_flyout_wrapper';
import { SyntheticsAppProps } from './contexts';
import {
@@ -30,7 +29,7 @@ import {
import { PageRouter } from './routes';
import { store, storage, setBasePath } from './state';
-import { kibanaService } from './utils/kibana_service';
+import { kibanaService } from '../../utils/kibana_service';
import { ActionMenu } from './components/common/header/action_menu';
const Application = (props: SyntheticsAppProps) => {
@@ -99,7 +98,6 @@ const Application = (props: SyntheticsAppProps) => {
application={core.application}
>
-
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx
index 51c186c352a5b..71d86cc53a76b 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/rtl_helpers.tsx
@@ -36,7 +36,7 @@ import {
SyntheticsRefreshContextProvider,
SyntheticsStartupPluginsContextProvider,
} from '../../contexts';
-import { kibanaService } from '../kibana_service';
+import { kibanaService } from '../../../../utils/kibana_service';
type DeepPartial = {
[P in keyof T]?: DeepPartial;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.test.ts b/x-pack/plugins/synthetics/public/hooks/use_breakpoints.test.ts
similarity index 100%
rename from x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.test.ts
rename to x-pack/plugins/synthetics/public/hooks/use_breakpoints.test.ts
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.ts b/x-pack/plugins/synthetics/public/hooks/use_breakpoints.ts
similarity index 100%
rename from x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_breakpoints.ts
rename to x-pack/plugins/synthetics/public/hooks/use_breakpoints.ts
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx
index 57ae3a6514505..4efaf26a7ac11 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.test.tsx
@@ -11,9 +11,9 @@ import 'jest-styled-components';
import { render } from '../lib/helper/rtl_helpers';
import { UptimePageTemplateComponent } from './uptime_page_template';
import { OVERVIEW_ROUTE } from '../../../common/constants';
-import { useBreakpoints } from '../../apps/synthetics/hooks/use_breakpoints';
+import { useBreakpoints } from '../../hooks/use_breakpoints';
-jest.mock('../../apps/synthetics/hooks/use_breakpoints', () => {
+jest.mock('../../hooks/use_breakpoints', () => {
const down = jest.fn().mockReturnValue(false);
return {
useBreakpoints: () => ({ down }),
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx
index ade54e1e6f61a..fa3ad7e0805e8 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/app/uptime_page_template.tsx
@@ -16,7 +16,7 @@ import { useNoDataConfig } from './use_no_data_config';
import { EmptyStateLoading } from '../components/overview/empty_state/empty_state_loading';
import { EmptyStateError } from '../components/overview/empty_state/empty_state_error';
import { useHasData } from '../components/overview/empty_state/use_has_data';
-import { useBreakpoints } from '../../apps/synthetics/hooks/use_breakpoints';
+import { useBreakpoints } from '../../hooks/use_breakpoints';
interface Props {
path: string;
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx
index 0e4d03e3ce438..73996c4e3a1b7 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor/ping_list/columns/ping_timestamp/step_image_caption.tsx
@@ -10,7 +10,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { ScreenshotRefImageData } from '../../../../../../../common/runtime_types';
-import { useBreakpoints } from '../../../../../../apps/synthetics/hooks/use_breakpoints';
+import { useBreakpoints } from '../../../../../../hooks/use_breakpoints';
import { nextAriaLabel, prevAriaLabel } from './translations';
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx
index 0c1d56be587a4..4b9374e991e6b 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list.tsx
@@ -27,7 +27,7 @@ import {
TCPSimpleFields,
} from '../../../../../common/runtime_types';
import { UptimeSettingsContext } from '../../../contexts';
-import { useBreakpoints } from '../../../../apps/synthetics/hooks/use_breakpoints';
+import { useBreakpoints } from '../../../../hooks/use_breakpoints';
import { MonitorManagementList as MonitorManagementListState } from '../../../state/reducers/monitor_management';
import * as labels from '../../overview/monitor_list/translations';
import { Actions } from './actions';
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx
index c09da77a6f559..f9d98b7b640c6 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/pages/synthetics/checks_navigation.tsx
@@ -12,7 +12,7 @@ import { useHistory } from 'react-router-dom';
import moment from 'moment';
import { SyntheticsJourneyApiResponse } from '../../../../common/runtime_types/ping';
import { getShortTimeStamp } from '../../components/overview/monitor_list/columns/monitor_status_column';
-import { useBreakpoints } from '../../../apps/synthetics/hooks/use_breakpoints';
+import { useBreakpoints } from '../../../hooks/use_breakpoints';
interface Props {
timestamp: string;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/api_service.ts b/x-pack/plugins/synthetics/public/utils/api_service/api_service.ts
similarity index 100%
rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/api_service.ts
rename to x-pack/plugins/synthetics/public/utils/api_service/api_service.ts
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/index.ts b/x-pack/plugins/synthetics/public/utils/api_service/index.ts
similarity index 100%
rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/api_service/index.ts
rename to x-pack/plugins/synthetics/public/utils/api_service/index.ts
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/index.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/index.ts
similarity index 100%
rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/index.ts
rename to x-pack/plugins/synthetics/public/utils/kibana_service/index.ts
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/kibana_service.ts b/x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts
similarity index 100%
rename from x-pack/plugins/synthetics/public/apps/synthetics/utils/kibana_service/kibana_service.ts
rename to x-pack/plugins/synthetics/public/utils/kibana_service/kibana_service.ts
diff --git a/x-pack/plugins/timelines/common/types/timeline/index.ts b/x-pack/plugins/timelines/common/types/timeline/index.ts
index 867264fa81546..528c6e4293cf4 100644
--- a/x-pack/plugins/timelines/common/types/timeline/index.ts
+++ b/x-pack/plugins/timelines/common/types/timeline/index.ts
@@ -314,7 +314,7 @@ export enum TimelineId {
usersPageExternalAlerts = 'users-page-external-alerts',
hostsPageEvents = 'hosts-page-events',
hostsPageExternalAlerts = 'hosts-page-external-alerts',
- hostsPageSessions = 'hosts-page-sessions',
+ hostsPageSessions = 'hosts-page-sessions-v2',
detectionsRulesDetailsPage = 'detections-rules-details-page',
detectionsPage = 'detections-page',
networkPageExternalAlerts = 'network-page-external-alerts',
diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts
index c4627b3accd71..8e0b7e995dbcd 100644
--- a/x-pack/plugins/timelines/public/store/t_grid/types.ts
+++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts
@@ -46,7 +46,7 @@ export enum TimelineId {
usersPageExternalAlerts = 'users-page-external-alerts',
hostsPageEvents = 'hosts-page-events',
hostsPageExternalAlerts = 'hosts-page-external-alerts',
- hostsPageSessions = 'hosts-page-sessions',
+ hostsPageSessions = 'hosts-page-sessions-v2',
detectionsRulesDetailsPage = 'detections-rules-details-page',
detectionsPage = 'detections-page',
networkPageExternalAlerts = 'network-page-external-alerts',
diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts
index 980f19ac2950c..d450daadf4689 100644
--- a/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts
+++ b/x-pack/plugins/timelines/server/search_strategy/timeline/index.ts
@@ -209,17 +209,13 @@ const timelineSessionsSearchStrategy = ({
};
const collapse = {
- field: 'process.entity_id',
- inner_hits: {
- name: 'last_event',
- size: 1,
- sort: [{ '@timestamp': 'desc' }],
- },
+ field: 'process.entry_leader.entity_id',
};
+
const aggs = {
total: {
cardinality: {
- field: 'process.entity_id',
+ field: 'process.entry_leader.entity_id',
},
},
};
diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts
index 0a0b8cdeab208..33f5fdc44afcd 100644
--- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts
+++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts
@@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({
rulesListDatagrid: true,
internalAlertsTable: false,
internalShareableComponentsSandbox: false,
+ ruleTagFilter: false,
ruleStatusFilter: false,
rulesDetailLogs: true,
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx
new file mode 100644
index 0000000000000..58603fdb8f178
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx
@@ -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 React, { useState } from 'react';
+import { EuiSpacer } from '@elastic/eui';
+import { getRuleTagFilterLazy } from '../../../common/get_rule_tag_filter';
+
+export const RuleTagFilterSandbox = () => {
+ const [selectedTags, setSelectedTags] = useState([]);
+
+ return (
+
+ {getRuleTagFilterLazy({
+ tags: ['tag1', 'tag2', 'tag3', 'tag4'],
+ selectedTags,
+ onChange: setSelectedTags,
+ })}
+
+
selected tags: {JSON.stringify(selectedTags)}
+
+ );
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx
index bedcbb03045a5..668b1ccb5aa69 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx
@@ -7,6 +7,7 @@
import React from 'react';
import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox';
+import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox';
import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox';
import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox';
@@ -14,6 +15,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => {
return (
<>
+
>
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts
index ab8f1b565c888..5377e4269f46e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts
@@ -6,7 +6,7 @@
*/
import { httpServiceMock } from '@kbn/core/public/mocks';
-import { loadRuleAggregations } from './aggregate';
+import { loadRuleAggregations, loadRuleTags } from './aggregate';
const http = httpServiceMock.createStartContract();
@@ -289,4 +289,68 @@ describe('loadRuleAggregations', () => {
]
`);
});
+
+ test('should call aggregate API with tagsFilter', async () => {
+ const resolvedValue = {
+ rule_execution_status: {
+ ok: 4,
+ active: 2,
+ error: 1,
+ pending: 1,
+ unknown: 0,
+ },
+ };
+ http.get.mockResolvedValueOnce(resolvedValue);
+
+ const result = await loadRuleAggregations({
+ http,
+ searchText: 'baz',
+ tagsFilter: ['a', 'b', 'c'],
+ });
+
+ expect(result).toEqual({
+ ruleExecutionStatus: {
+ ok: 4,
+ active: 2,
+ error: 1,
+ pending: 1,
+ unknown: 0,
+ },
+ });
+
+ expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "/internal/alerting/rules/_aggregate",
+ Object {
+ "query": Object {
+ "default_search_operator": "AND",
+ "filter": "alert.attributes.tags:(a or b or c)",
+ "search": "baz",
+ "search_fields": "[\\"name\\",\\"tags\\"]",
+ },
+ },
+ ]
+ `);
+ });
+
+ test('loadRuleTags should call the aggregate API with no filters', async () => {
+ const resolvedValue = {
+ rule_tags: ['a', 'b', 'c'],
+ };
+ http.get.mockResolvedValueOnce(resolvedValue);
+
+ const result = await loadRuleTags({
+ http,
+ });
+
+ expect(result).toEqual({
+ ruleTags: ['a', 'b', 'c'],
+ });
+
+ expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "/internal/alerting/rules/_aggregate",
+ ]
+ `);
+ });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts
index 9548445d0df9c..1df6177443657 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts
@@ -10,11 +10,16 @@ import { RuleAggregations, RuleStatus } from '../../../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
import { mapFiltersToKql } from './map_filters_to_kql';
+export interface RuleTagsAggregations {
+ ruleTags: string[];
+}
+
const rewriteBodyRes: RewriteRequestCase = ({
rule_execution_status: ruleExecutionStatus,
rule_enabled_status: ruleEnabledStatus,
rule_muted_status: ruleMutedStatus,
rule_snoozed_status: ruleSnoozedStatus,
+ rule_tags: ruleTags,
...rest
}: any) => ({
...rest,
@@ -22,8 +27,23 @@ const rewriteBodyRes: RewriteRequestCase = ({
ruleEnabledStatus,
ruleMutedStatus,
ruleSnoozedStatus,
+ ruleTags,
+});
+
+const rewriteTagsBodyRes: RewriteRequestCase = ({
+ rule_tags: ruleTags,
+}: any) => ({
+ ruleTags,
});
+// TODO: https://github.com/elastic/kibana/issues/131682
+export async function loadRuleTags({ http }: { http: HttpSetup }): Promise {
+ const res = await http.get>(
+ `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`
+ );
+ return rewriteTagsBodyRes(res);
+}
+
export async function loadRuleAggregations({
http,
searchText,
@@ -31,6 +51,7 @@ export async function loadRuleAggregations({
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
+ tagsFilter,
}: {
http: HttpSetup;
searchText?: string;
@@ -38,12 +59,14 @@ export async function loadRuleAggregations({
actionTypesFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
+ tagsFilter?: string[];
}): Promise {
const filters = mapFiltersToKql({
typesFilter,
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
+ tagsFilter,
});
const res = await http.get>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts
index 89ede79f4a21d..c9834dd140ea4 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts
@@ -7,7 +7,7 @@
export { alertingFrameworkHealth } from './health';
export { mapFiltersToKql } from './map_filters_to_kql';
-export { loadRuleAggregations } from './aggregate';
+export { loadRuleAggregations, loadRuleTags } from './aggregate';
export { createRule } from './create';
export { deleteRules } from './delete';
export { disableRule, disableRules } from './disable';
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts
index df762d05e0eff..f67a27ef5409c 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts
@@ -88,6 +88,14 @@ describe('mapFiltersToKql', () => {
]);
});
+ test('should handle tagsFilter', () => {
+ expect(
+ mapFiltersToKql({
+ tagsFilter: ['a', 'b', 'c'],
+ })
+ ).toEqual(['alert.attributes.tags:(a or b or c)']);
+ });
+
test('should handle typesFilter and actionTypesFilter', () => {
expect(
mapFiltersToKql({
@@ -100,17 +108,19 @@ describe('mapFiltersToKql', () => {
]);
});
- test('should handle typesFilter, actionTypesFilter and ruleExecutionStatusesFilter', () => {
+ test('should handle typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, and tagsFilter', () => {
expect(
mapFiltersToKql({
typesFilter: ['type', 'filter'],
actionTypesFilter: ['action', 'types', 'filter'],
ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'],
+ tagsFilter: ['a', 'b', 'c'],
})
).toEqual([
'alert.attributes.alertTypeId:(type or filter)',
'(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })',
'alert.attributes.executionStatus.status:(alert or statuses or filter)',
+ 'alert.attributes.tags:(a or b or c)',
]);
});
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts
index 0e64f5500454f..ff2a49e3a5e45 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts
@@ -25,9 +25,11 @@ export const mapFiltersToKql = ({
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
+ tagsFilter,
}: {
typesFilter?: string[];
actionTypesFilter?: string[];
+ tagsFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
}): string[] => {
@@ -68,6 +70,9 @@ export const mapFiltersToKql = ({
filters.push(`${enablementFilter} or ${snoozedFilter}`);
}
}
+ if (tagsFilter && tagsFilter.length) {
+ filters.push(`alert.attributes.tags:(${tagsFilter.join(' or ')})`);
+ }
return filters;
};
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts
index 8adc92738b7c6..2a20c9d9469f5 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts
@@ -336,4 +336,42 @@ describe('loadRules', () => {
]
`);
});
+
+ test('should call find API with tagsFilter', async () => {
+ const resolvedValue = {
+ page: 1,
+ per_page: 10,
+ total: 0,
+ data: [],
+ };
+ http.get.mockResolvedValueOnce(resolvedValue);
+ const result = await loadRules({
+ http,
+ tagsFilter: ['a', 'b', 'c'],
+ page: { index: 0, size: 10 },
+ });
+ expect(result).toEqual({
+ page: 1,
+ perPage: 10,
+ total: 0,
+ data: [],
+ });
+ expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
+ Array [
+ "/internal/alerting/rules/_find",
+ Object {
+ "query": Object {
+ "default_search_operator": "AND",
+ "filter": "alert.attributes.tags:(a or b or c)",
+ "page": 1,
+ "per_page": 10,
+ "search": undefined,
+ "search_fields": undefined,
+ "sort_field": "name",
+ "sort_order": "asc",
+ },
+ },
+ ]
+ `);
+ });
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts
index bdbdcf2f094b2..6e527989cc91f 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts
@@ -23,6 +23,7 @@ export async function loadRules({
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
+ tagsFilter,
sort = { field: 'name', direction: 'asc' },
}: {
http: HttpSetup;
@@ -30,6 +31,7 @@ export async function loadRules({
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
+ tagsFilter?: string[];
ruleExecutionStatusesFilter?: string[];
ruleStatusesFilter?: RuleStatus[];
sort?: Sorting;
@@ -42,6 +44,7 @@ export async function loadRules({
const filters = mapFiltersToKql({
typesFilter,
actionTypesFilter,
+ tagsFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx
index e41c2a73a5124..9ab31ae12402f 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx
@@ -32,6 +32,9 @@ export const ActionForm = suspendedComponentWithProps(
export const RuleStatusDropdown = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rule_status_dropdown'))
);
+export const RuleTagFilter = suspendedComponentWithProps(
+ lazy(() => import('./rules_list/components/rule_tag_filter'))
+);
export const RuleStatusFilter = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rule_status_filter'))
);
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx
new file mode 100644
index 0000000000000..a6b60b1099391
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { mountWithIntl } from '@kbn/test-jest-helpers';
+import { EuiFilterButton, EuiSelectable } from '@elastic/eui';
+import { RuleTagFilter } from './rule_tag_filter';
+
+const onChangeMock = jest.fn();
+
+const tags = ['a', 'b', 'c', 'd', 'e', 'f'];
+
+describe('rule_tag_filter', () => {
+ beforeEach(() => {
+ onChangeMock.mockReset();
+ });
+
+ it('renders correctly', () => {
+ const wrapper = mountWithIntl(
+
+ );
+
+ expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy();
+ expect(wrapper.find('.euiNotificationBadge').text()).toEqual('0');
+ });
+
+ it('can open the popover correctly', () => {
+ const wrapper = mountWithIntl(
+
+ );
+
+ expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeFalsy();
+
+ wrapper.find(EuiFilterButton).simulate('click');
+
+ expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeTruthy();
+ expect(wrapper.find('li').length).toEqual(tags.length);
+ });
+
+ it('can select tags', () => {
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(EuiFilterButton).simulate('click');
+
+ wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click');
+ expect(onChangeMock).toHaveBeenCalledWith(['a']);
+
+ wrapper.setProps({
+ selectedTags: ['a'],
+ });
+
+ wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click');
+ expect(onChangeMock).toHaveBeenCalledWith([]);
+
+ wrapper.find('[data-test-subj="ruleTagFilterOption-b"]').at(0).simulate('click');
+ expect(onChangeMock).toHaveBeenCalledWith(['a', 'b']);
+ });
+
+ it('renders selected tags even if they get deleted from the tags array', () => {
+ const selectedTags = ['g', 'h'];
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(EuiFilterButton).simulate('click');
+
+ expect(wrapper.find(EuiSelectable).props().options.length).toEqual(
+ tags.length + selectedTags.length
+ );
+ });
+});
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx
new file mode 100644
index 0000000000000..6aa8aa8c69213
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useMemo, useState, useCallback } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import {
+ EuiSelectable,
+ EuiFilterGroup,
+ EuiFilterButton,
+ EuiPopover,
+ EuiSelectableProps,
+ EuiSelectableOption,
+ EuiSpacer,
+} from '@elastic/eui';
+
+export interface RuleTagFilterProps {
+ tags: string[];
+ selectedTags: string[];
+ isLoading?: boolean;
+ loadingMessage?: EuiSelectableProps['loadingMessage'];
+ noMatchesMessage?: EuiSelectableProps['noMatchesMessage'];
+ emptyMessage?: EuiSelectableProps['emptyMessage'];
+ errorMessage?: EuiSelectableProps['errorMessage'];
+ dataTestSubj?: string;
+ selectableDataTestSubj?: string;
+ optionDataTestSubj?: (tag: string) => string;
+ buttonDataTestSubj?: string;
+ onChange: (tags: string[]) => void;
+}
+
+const getOptionDataTestSubj = (tag: string) => `ruleTagFilterOption-${tag}`;
+
+export const RuleTagFilter = (props: RuleTagFilterProps) => {
+ const {
+ tags = [],
+ selectedTags = [],
+ isLoading = false,
+ loadingMessage,
+ noMatchesMessage,
+ emptyMessage,
+ errorMessage,
+ dataTestSubj = 'ruleTagFilter',
+ selectableDataTestSubj = 'ruleTagFilterSelectable',
+ optionDataTestSubj = getOptionDataTestSubj,
+ buttonDataTestSubj = 'ruleTagFilterButton',
+ onChange = () => {},
+ } = props;
+
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+
+ const allTags = useMemo(() => {
+ return [...new Set([...tags, ...selectedTags])].sort();
+ }, [tags, selectedTags]);
+
+ const options: EuiSelectableOption[] = useMemo(
+ () =>
+ allTags.map((tag) => ({
+ label: tag,
+ checked: selectedTags.includes(tag) ? 'on' : undefined,
+ 'data-test-subj': optionDataTestSubj(tag),
+ })),
+ [allTags, selectedTags, optionDataTestSubj]
+ );
+
+ const onChangeInternal = useCallback(
+ (newOptions: EuiSelectableOption[]) => {
+ const newSelectedTags = newOptions.reduce((result, option) => {
+ if (option.checked === 'on') {
+ result = [...result, option.label];
+ }
+ return result;
+ }, []);
+
+ onChange(newSelectedTags);
+ },
+ [onChange]
+ );
+
+ const onClosePopover = () => {
+ setIsPopoverOpen(!isPopoverOpen);
+ };
+
+ const renderButton = () => {
+ return (
+ 0}
+ numActiveFilters={selectedTags.length}
+ numFilters={selectedTags.length}
+ onClick={onClosePopover}
+ >
+
+
+ );
+ };
+
+ return (
+
+
+
+ {(list, search) => (
+ <>
+ {search}
+
+ {list}
+ >
+ )}
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export { RuleTagFilter as default };
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx
index 52c6e2d3ed149..12e1b0f1e4a6e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx
@@ -33,6 +33,7 @@ jest.mock('../../../lib/rule_api', () => ({
loadRules: jest.fn(),
loadRuleTypes: jest.fn(),
loadRuleAggregations: jest.fn(),
+ loadRuleTags: jest.fn(),
alertingFrameworkHealth: jest.fn(() => ({
isSufficientlySecure: true,
hasPermanentEncryptionKey: true,
@@ -63,7 +64,10 @@ jest.mock('../../../lib/capabilities', () => ({
jest.mock('../../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled: jest.fn(),
}));
-const { loadRules, loadRuleTypes, loadRuleAggregations } =
+
+const ruleTags = ['a', 'b', 'c', 'd'];
+
+const { loadRules, loadRuleTypes, loadRuleAggregations, loadRuleTags } =
jest.requireMock('../../../lib/rule_api');
const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api');
const actionTypeRegistry = actionTypeRegistryMock.create();
@@ -395,6 +399,10 @@ describe('rules_list component with items', () => {
ruleEnabledStatus: { enabled: 2, disabled: 0 },
ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 },
ruleMutedStatus: { muted: 0, unmuted: 2 },
+ ruleTags,
+ });
+ loadRuleTags.mockResolvedValue({
+ ruleTags,
});
const ruleTypeMock: RuleTypeModel = {
@@ -842,6 +850,40 @@ describe('rules_list component with items', () => {
expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']);
});
+
+ it('does not render the tag filter is the feature flag is off', async () => {
+ await setup();
+ expect(wrapper.find('[data-test-subj="ruleTagFilter"]').exists()).toBeFalsy();
+ });
+
+ it('renders the tag filter if the experiment is on', async () => {
+ (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true);
+ await setup();
+ expect(wrapper.find('[data-test-subj="ruleTagFilter"]').exists()).toBeTruthy();
+ });
+
+ it('can filter by tags', async () => {
+ (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true);
+ loadRules.mockReset();
+ await setup();
+
+ expect(loadRules.mock.calls[0][0].tagsFilter).toEqual([]);
+
+ wrapper.find('[data-test-subj="ruleTagFilterButton"] button').simulate('click');
+
+ const tagFilterListItems = wrapper.find(
+ '[data-test-subj="ruleTagFilterSelectable"] .euiSelectableListItem'
+ );
+ expect(tagFilterListItems.length).toEqual(ruleTags.length);
+
+ tagFilterListItems.at(0).simulate('click');
+
+ expect(loadRules.mock.calls[1][0].tagsFilter).toEqual(['a']);
+
+ tagFilterListItems.at(1).simulate('click');
+
+ expect(loadRules.mock.calls[2][0].tagsFilter).toEqual(['a', 'b']);
+ });
});
describe('rules_list component empty with show only capability', () => {
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx
index b1255600b68de..a5b9661835131 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx
@@ -73,6 +73,7 @@ import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_stat
import {
loadRules,
loadRuleAggregations,
+ loadRuleTags,
loadRuleTypes,
disableRule,
enableRule,
@@ -99,6 +100,7 @@ import { RuleDurationFormat } from './rule_duration_format';
import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils';
import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils';
import { triggersActionsUiConfig } from '../../../../common/lib/config_api';
+import { RuleTagFilter } from './rule_tag_filter';
import { RuleStatusFilter } from './rule_status_filter';
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
@@ -158,6 +160,8 @@ export const RulesList: React.FunctionComponent = () => {
const [actionTypesFilter, setActionTypesFilter] = useState([]);
const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]);
const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]);
+ const [tags, setTags] = useState([]);
+ const [tagsFilter, setTagsFilter] = useState([]);
const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false);
const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null);
@@ -167,6 +171,7 @@ export const RulesList: React.FunctionComponent = () => {
);
const [showErrors, setShowErrors] = useState(false);
+ const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter');
const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter');
useEffect(() => {
@@ -233,6 +238,7 @@ export const RulesList: React.FunctionComponent = () => {
JSON.stringify(actionTypesFilter),
JSON.stringify(ruleExecutionStatusesFilter),
JSON.stringify(ruleStatusesFilter),
+ JSON.stringify(tagsFilter),
]);
useEffect(() => {
@@ -293,8 +299,10 @@ export const RulesList: React.FunctionComponent = () => {
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
+ tagsFilter,
sort,
});
+ await loadRuleTagsAggs();
await loadRuleAggs();
setRulesState({
isLoading: false,
@@ -311,7 +319,8 @@ export const RulesList: React.FunctionComponent = () => {
isEmpty(typesFilter) &&
isEmpty(actionTypesFilter) &&
isEmpty(ruleExecutionStatusesFilter) &&
- isEmpty(ruleStatusesFilter)
+ isEmpty(ruleStatusesFilter) &&
+ isEmpty(tagsFilter)
);
setNoData(rulesResponse.data.length === 0 && !isFilterApplied);
@@ -339,6 +348,7 @@ export const RulesList: React.FunctionComponent = () => {
actionTypesFilter,
ruleExecutionStatusesFilter,
ruleStatusesFilter,
+ tagsFilter,
});
if (rulesAggs?.ruleExecutionStatus) {
setRulesStatusesTotal(rulesAggs.ruleExecutionStatus);
@@ -355,6 +365,24 @@ export const RulesList: React.FunctionComponent = () => {
}
}
+ async function loadRuleTagsAggs() {
+ if (!isRuleTagFilterEnabled) {
+ return;
+ }
+ try {
+ const ruleTagsAggs = await loadRuleTags({ http });
+ if (ruleTagsAggs?.ruleTags) {
+ setTags(ruleTagsAggs.ruleTags);
+ }
+ } catch (e) {
+ toasts.addDanger({
+ title: i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', {
+ defaultMessage: 'Unable to load rule tags',
+ }),
+ });
+ }
+ }
+
const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => {
return (
{
sortable: false,
width: '50px',
'data-test-subj': 'rulesTableCell-tagsPopover',
- render: (tags: string[], item: RuleTableItem) => {
- return tags.length > 0 ? (
+ render: (ruleTags: string[], item: RuleTableItem) => {
+ return ruleTags.length > 0 ? (
setTagPopoverOpenIndex(item.index)}
onClose={() => setTagPopoverOpenIndex(-1)}
/>
@@ -940,6 +968,13 @@ export const RulesList: React.FunctionComponent = () => {
);
};
+ const getRuleTagFilter = () => {
+ if (isRuleTagFilterEnabled) {
+ return [];
+ }
+ return [];
+ };
+
const getRuleStatusFilter = () => {
if (isRuleStatusFilterEnabled) {
return [
@@ -960,6 +995,7 @@ export const RulesList: React.FunctionComponent = () => {
})
)}
/>,
+ ...getRuleTagFilter(),
...getRuleStatusFilter(),
{
rulesListDatagrid: true,
internalAlertsTable: true,
rulesDetailLogs: true,
+ ruleTagFilter: true,
ruleStatusFilter: true,
internalShareableComponentsSandbox: true,
},
@@ -39,6 +40,10 @@ describe('getIsExperimentalFeatureEnabled', () => {
expect(result).toEqual(true);
+ result = getIsExperimentalFeatureEnabled('ruleTagFilter');
+
+ expect(result).toEqual(true);
+
result = getIsExperimentalFeatureEnabled('ruleStatusFilter');
expect(result).toEqual(true);
diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx
new file mode 100644
index 0000000000000..ccca277ef10ba
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { RuleTagFilter } from '../application/sections';
+import type { RuleTagFilterProps } from '../application/sections/rules_list/components/rule_tag_filter';
+
+export const getRuleTagFilterLazy = (props: RuleTagFilterProps) => {
+ return ;
+};
diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts
index cb79a1509a6c1..003748f7d421e 100644
--- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts
@@ -26,6 +26,7 @@ import {
} from './types';
import { getAlertsTableLazy } from './common/get_alerts_table';
import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown';
+import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
@@ -66,6 +67,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleStatusDropdown: (props) => {
return getRuleStatusDropdownLazy(props);
},
+ getRuleTagFilter: (props) => {
+ return getRuleTagFilterLazy(props);
+ },
getRuleStatusFilter: (props) => {
return getRuleStatusFilterLazy(props);
},
diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
index 1d9c3c07e44ca..c95dd73102fd9 100644
--- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
@@ -31,6 +31,7 @@ import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout';
import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout';
import { getAlertsTableLazy } from './common/get_alerts_table';
import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown';
+import { getRuleTagFilterLazy } from './common/get_rule_tag_filter';
import { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
@@ -48,6 +49,7 @@ import type {
ConnectorEditFlyoutProps,
AlertsTableProps,
RuleStatusDropdownProps,
+ RuleTagFilterProps,
RuleStatusFilterProps,
RuleTagBadgeProps,
AlertsTableConfigurationRegistry,
@@ -80,6 +82,7 @@ export interface TriggersAndActionsUIPublicPluginStart {
) => ReactElement;
getAlertsTable: (props: AlertsTableProps) => ReactElement;
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement;
+ getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement;
getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement;
getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement;
}
@@ -255,6 +258,9 @@ export class Plugin
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => {
return getRuleStatusDropdownLazy(props);
},
+ getRuleTagFilter: (props: RuleTagFilterProps) => {
+ return getRuleTagFilterLazy(props);
+ },
getRuleStatusFilter: (props: RuleStatusFilterProps) => {
return getRuleStatusFilterLazy(props);
},
diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts
index 25efbfb6ecc38..ef7ea7096961b 100644
--- a/x-pack/plugins/triggers_actions_ui/public/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/types.ts
@@ -48,6 +48,7 @@ import {
import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common';
import { TypeRegistry } from './application/type_registry';
import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown';
+import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter';
import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter';
import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge';
@@ -82,6 +83,7 @@ export type {
ResolvedRule,
SanitizedRule,
RuleStatusDropdownProps,
+ RuleTagFilterProps,
RuleStatusFilterProps,
RuleTagBadgeProps,
};
diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
index 4525768a0fb42..82516bf4a417d 100644
--- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
+++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts
@@ -15,6 +15,7 @@ import { ActionType } from '@kbn/actions-plugin/server';
import { initPlugin as initPagerduty } from './pagerduty_simulation';
import { initPlugin as initSwimlane } from './swimlane_simulation';
import { initPlugin as initServiceNow } from './servicenow_simulation';
+import { initPlugin as initServiceNowOAuth } from './servicenow_oauth_simulation';
import { initPlugin as initJira } from './jira_simulation';
import { initPlugin as initResilient } from './resilient_simulation';
import { initPlugin as initSlack } from './slack_simulation';
@@ -49,6 +50,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] {
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.MS_EXCHANGE}/users/test@/sendMail`);
allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.MS_EXCHANGE}/1234567/oauth2/v2.0/token`);
+ allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/oauth_token.do`);
return allPaths;
}
@@ -129,6 +131,10 @@ export class FixturePlugin implements Plugin,
+ res: KibanaResponseFactory
+ ): Promise> {
+ return jsonResponse(res, 200, {
+ access_token: 'tokentokentoken',
+ expires_in: 3660,
+ token_type: 'Bearer',
+ });
+ }
+ );
+}
+
+function jsonResponse(
+ res: KibanaResponseFactory,
+ code: number,
+ object: Record = {}
+) {
+ return res.custom>({ body: object, statusCode: code });
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts
new file mode 100644
index 0000000000000..6053f78ea76a4
--- /dev/null
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/builtin_action_types/oauth_access_token.ts
@@ -0,0 +1,170 @@
+/*
+ * 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 fs from 'fs';
+import expect from '@kbn/expect';
+import { promisify } from 'util';
+import httpProxy from 'http-proxy';
+import { KBN_KEY_PATH } from '@kbn/dev-utils';
+import { FtrProviderContext } from '../../../../../common/ftr_provider_context';
+import { getHttpProxyServer } from '../../../../../common/lib/get_proxy_server';
+import {
+ ExternalServiceSimulator,
+ getExternalServiceSimulatorPath,
+} from '../../../../../common/fixtures/plugins/actions_simulators/server/plugin';
+
+// eslint-disable-next-line import/no-default-export
+export default function oAuthAccessTokenTest({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+ const kibanaServer = getService('kibanaServer');
+
+ describe('get oauth access token', () => {
+ let servicenowSimulatorURL: string = '';
+ let proxyServer: httpProxy | undefined;
+ let testPrivateKey: string;
+ const configService = getService('config');
+
+ // need to wait for kibanaServer to settle ...
+ before(async () => {
+ testPrivateKey = await promisify(fs.readFile)(KBN_KEY_PATH, 'utf8');
+ servicenowSimulatorURL = kibanaServer.resolveUrl(
+ getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW)
+ );
+ proxyServer = await getHttpProxyServer(
+ kibanaServer.resolveUrl('/'),
+ configService.get('kbnTestServer.serverArgs'),
+ () => {}
+ );
+ });
+
+ after(() => {
+ if (proxyServer) {
+ proxyServer.close();
+ }
+ });
+
+ it('should return 200 when requesting a JWT access token with OAuth credentials', async () => {
+ const { body: accessToken } = await supertest
+ .post('/internal/actions/connector/_oauth_access_token')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ type: 'jwt',
+ options: {
+ tokenUrl: `${servicenowSimulatorURL}/oauth_token.do`,
+ config: {
+ clientId: 'abc',
+ userIdentifierValue: 'elastic',
+ jwtKeyId: 'def',
+ },
+ secrets: {
+ clientSecret: 'xyz',
+ privateKey: testPrivateKey,
+ },
+ },
+ })
+ .expect(200);
+
+ expect(accessToken).to.eql({ accessToken: 'Bearer tokentokentoken' });
+ });
+
+ it('should return 200 when requesting a Client Credentials access token with OAuth credentials', async () => {
+ const { body: accessToken } = await supertest
+ .post('/internal/actions/connector/_oauth_access_token')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ type: 'client',
+ options: {
+ tokenUrl: `${kibanaServer.resolveUrl(
+ getExternalServiceSimulatorPath(ExternalServiceSimulator.MS_EXCHANGE)
+ )}/1234567/oauth2/v2.0/token`,
+ scope: 'https://graph.microsoft.com/.default',
+ config: {
+ clientId: 'abc',
+ tenantId: '98765',
+ },
+ secrets: {
+ clientSecret: 'xyz',
+ },
+ },
+ })
+ .expect(200);
+
+ expect(accessToken).to.eql({ accessToken: 'Bearer asdadasd' });
+ });
+
+ it('should return 400 when given incorrect options for requesting Client Credentials access token with OAuth credentials', async () => {
+ await supertest
+ .post('/internal/actions/connector/_oauth_access_token')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ type: 'client',
+ options: {
+ tokenUrl: `${servicenowSimulatorURL}/oauth_token.do`,
+ config: {
+ clientId: 'abc',
+ userIdentifierValue: 'elastic',
+ jwtKeyId: 'def',
+ },
+ secrets: {
+ clientSecret: 'xyz',
+ privateKey: testPrivateKey,
+ },
+ },
+ })
+ .expect(400);
+ });
+
+ it('should return 400 when given incorrect options for requesting JWT access token with OAuth credentials', async () => {
+ await supertest
+ .post('/internal/actions/connector/_oauth_access_token')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ type: 'jwt',
+ options: {
+ tokenUrl: `${kibanaServer.resolveUrl(
+ getExternalServiceSimulatorPath(ExternalServiceSimulator.MS_EXCHANGE)
+ )}/1234567/oauth2/v2.0/token`,
+ scope: 'https://graph.microsoft.com/.default',
+ config: {
+ clientId: 'abc',
+ tenantId: '98765',
+ },
+ secrets: {
+ clientSecret: 'xyz',
+ },
+ },
+ })
+ .expect(400);
+ });
+
+ it('should return 400 when token url not included in allowlist', async () => {
+ const { body } = await supertest
+ .post('/internal/actions/connector/_oauth_access_token')
+ .set('kbn-xsrf', 'foo')
+ .send({
+ type: 'jwt',
+ options: {
+ tokenUrl: `https://servicenow.nonexistent.com/oauth_token.do`,
+ config: {
+ clientId: 'abc',
+ userIdentifierValue: 'elastic',
+ jwtKeyId: 'def',
+ },
+ secrets: {
+ clientSecret: 'xyz',
+ privateKey: testPrivateKey,
+ },
+ },
+ });
+
+ expect(body.statusCode).to.equal(400);
+ expect(body.message).to.equal(
+ `target url "https://servicenow.nonexistent.com/oauth_token.do" is not added to the Kibana config xpack.actions.allowedHosts`
+ );
+ });
+ });
+}
diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts
index 6d1ecdbee566c..9c1b6a4fd8299 100644
--- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts
+++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/actions/index.ts
@@ -25,6 +25,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo
loadTestFile(require.resolve('./builtin_action_types/pagerduty'));
loadTestFile(require.resolve('./builtin_action_types/swimlane'));
loadTestFile(require.resolve('./builtin_action_types/server_log'));
+ loadTestFile(require.resolve('./builtin_action_types/oauth_access_token'));
loadTestFile(require.resolve('./builtin_action_types/servicenow_itsm'));
loadTestFile(require.resolve('./builtin_action_types/servicenow_sir'));
loadTestFile(require.resolve('./builtin_action_types/servicenow_itom'));
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts
index 588e7132f268c..4424175e36953 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts
@@ -44,6 +44,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
rule_snoozed_status: {
snoozed: 0,
},
+ rule_tags: [],
});
});
@@ -122,6 +123,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
rule_snoozed_status: {
snoozed: 0,
},
+ rule_tags: ['foo'],
});
});
@@ -137,6 +139,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
{
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
+ tags: ['a', 'b'],
},
'ok'
);
@@ -153,6 +156,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
params: {
pattern: { instance: new Array(100).fill(true) },
},
+ tags: ['a', 'c', 'f'],
},
'active'
);
@@ -166,6 +170,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
{
rule_type_id: 'test.throw',
schedule: { interval: '1s' },
+ tags: ['b', 'c', 'd'],
},
'error'
);
@@ -202,6 +207,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext)
ruleSnoozedStatus: {
snoozed: 0,
},
+ ruleTags: ['a', 'b', 'c', 'd', 'f'],
});
});
});
diff --git a/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts b/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts
deleted file mode 100644
index ac8e88b6fefe3..0000000000000
--- a/x-pack/test/fleet_api_integration/apis/mock_http_server.d.ts
+++ /dev/null
@@ -1,10 +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.
- */
-
-// No types for mock-http-server available, but we don't need them.
-
-declare module 'mock-http-server';
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts
index 5f6c4501476bf..a036c25e3d657 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts
@@ -31,8 +31,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await testSubjects.click('rulesTab');
}
- // FLAKY: https://github.com/elastic/kibana/issues/131535
- describe.skip('rules list', function () {
+ describe('rules list', function () {
+ const assertRulesLength = async (length: number) => {
+ return await retry.try(async () => {
+ const rules = await pageObjects.triggersActionsUI.getAlertsList();
+ expect(rules.length).to.equal(length);
+ });
+ };
+
before(async () => {
await pageObjects.common.navigateToApp('triggersActions');
await testSubjects.click('rulesTab');
@@ -604,13 +610,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('should filter alerts by the rule status', async () => {
- const assertRulesLength = async (length: number) => {
- return await retry.try(async () => {
- const rules = await pageObjects.triggersActionsUI.getAlertsList();
- expect(rules.length).to.equal(length);
- });
- };
-
// Enabled alert
await createAlert({
supertest,
@@ -640,25 +639,94 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
// Select enabled
await testSubjects.click('ruleStatusFilterButton');
await testSubjects.click('ruleStatusFilterOption-enabled');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(1);
// Select disabled
await testSubjects.click('ruleStatusFilterOption-enabled');
await testSubjects.click('ruleStatusFilterOption-disabled');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(1);
// Select snoozed
await testSubjects.click('ruleStatusFilterOption-disabled');
await testSubjects.click('ruleStatusFilterOption-snoozed');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(1);
// Select disabled and snoozed
await testSubjects.click('ruleStatusFilterOption-disabled');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(2);
// Select all 3
await testSubjects.click('ruleStatusFilterOption-enabled');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
await assertRulesLength(3);
});
+
+ it('should filter alerts by the tag', async () => {
+ await createAlert({
+ supertest,
+ objectRemover,
+ overwrites: {
+ tags: ['a'],
+ },
+ });
+ await createAlert({
+ supertest,
+ objectRemover,
+ overwrites: {
+ tags: ['b'],
+ },
+ });
+ await createAlert({
+ supertest,
+ objectRemover,
+ overwrites: {
+ tags: ['a', 'b'],
+ },
+ });
+ await createAlert({
+ supertest,
+ objectRemover,
+ overwrites: {
+ tags: ['b', 'c'],
+ },
+ });
+ await createAlert({
+ supertest,
+ objectRemover,
+ overwrites: {
+ tags: ['c'],
+ },
+ });
+
+ await refreshAlertsList();
+ await testSubjects.click('ruleTagFilter');
+
+ // Select a -> selected: a
+ await testSubjects.click('ruleTagFilterOption-a');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
+ await assertRulesLength(2);
+
+ // Unselect a -> selected: none
+ await testSubjects.click('ruleTagFilterOption-a');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
+ await assertRulesLength(5);
+
+ // Select a, b -> selected: a, b
+ await testSubjects.click('ruleTagFilterOption-a');
+ await testSubjects.click('ruleTagFilterOption-b');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
+ await assertRulesLength(4);
+
+ // Unselect a, b, select c -> selected: c
+ await testSubjects.click('ruleTagFilterOption-a');
+ await testSubjects.click('ruleTagFilterOption-b');
+ await testSubjects.click('ruleTagFilterOption-c');
+ await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
+ await assertRulesLength(2);
+ });
});
};
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
index 73b084c2ce0e4..3b2803e17e184 100644
--- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts
@@ -16,6 +16,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => {
loadTestFile(require.resolve('./connectors'));
loadTestFile(require.resolve('./alerts_table'));
loadTestFile(require.resolve('./rule_status_dropdown'));
+ loadTestFile(require.resolve('./rule_tag_filter'));
loadTestFile(require.resolve('./rule_status_filter'));
loadTestFile(require.resolve('./rule_tag_badge'));
});
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts
new file mode 100644
index 0000000000000..77d57e2819db5
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts
@@ -0,0 +1,54 @@
+/*
+ * 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 { FtrProviderContext } from '../../ftr_provider_context';
+
+export default ({ getPageObjects, getService }: FtrProviderContext) => {
+ const testSubjects = getService('testSubjects');
+ const find = getService('find');
+ const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']);
+ const esArchiver = getService('esArchiver');
+
+ describe('Rule tag filter', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
+ await PageObjects.common.navigateToUrlWithBrowserHistory(
+ 'triggersActions',
+ '/__components_sandbox'
+ );
+ });
+ after(async () => {
+ await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
+ });
+
+ it('shoud load from shareable lazy loader', async () => {
+ await testSubjects.find('ruleTagFilter');
+ const exists = await testSubjects.exists('ruleTagFilter');
+ expect(exists).to.be(true);
+ });
+
+ it('should allow tag filters to be selected', async () => {
+ let badge = await find.byCssSelector('.euiFilterButton__notification');
+ expect(await badge.getVisibleText()).to.be('0');
+
+ await testSubjects.click('ruleTagFilter');
+ await testSubjects.click('ruleTagFilterOption-tag1');
+
+ badge = await find.byCssSelector('.euiFilterButton__notification');
+ expect(await badge.getVisibleText()).to.be('1');
+
+ await testSubjects.click('ruleTagFilterOption-tag2');
+
+ badge = await find.byCssSelector('.euiFilterButton__notification');
+ expect(await badge.getVisibleText()).to.be('2');
+
+ await testSubjects.click('ruleTagFilterOption-tag1');
+ expect(await badge.getVisibleText()).to.be('1');
+ });
+ });
+};
diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts
index 4872d2fd6fa38..62984ace526fb 100644
--- a/x-pack/test/functional_with_es_ssl/config.ts
+++ b/x-pack/test/functional_with_es_ssl/config.ts
@@ -76,6 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
`--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([
'internalAlertsTable',
'internalShareableComponentsSandbox',
+ 'ruleTagFilter',
'ruleStatusFilter',
])}`,
`--xpack.alerting.rules.minimumScheduleInterval.value="2s"`,
diff --git a/yarn.lock b/yarn.lock
index 439a288a9db59..45307af4aa044 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6660,11 +6660,6 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
-"@types/parse-link-header@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a"
- integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA==
-
"@types/parse5@*", "@types/parse5@^5.0.0":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
@@ -7096,13 +7091,6 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74"
integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
-"@types/tar-fs@^1.16.1":
- version "1.16.1"
- resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-1.16.1.tgz#6e3fba276c173e365ae91e55f7b797a0e64298e5"
- integrity sha512-uQQIaa8ukcKf/1yy2kzfP1PF+7jEZghFDKpDvgtsYo/mbqM1g4Qza1Y5oAw6kJMa7eLA/HkmxUsDqb2sWKVF9g==
- dependencies:
- "@types/node" "*"
-
"@types/tar@^4.0.5":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.5.tgz#5f953f183e36a15c6ce3f336568f6051b7b183f3"
@@ -8587,7 +8575,7 @@ async@^1.4.2:
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
-async@^2.1.4, async@^2.6.2:
+async@^2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
@@ -9189,7 +9177,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
-body-parser@1.19.0, body-parser@^1.18.1, body-parser@^1.18.3:
+body-parser@1.19.0, body-parser@^1.18.3:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@@ -10833,16 +10821,6 @@ connect-history-api-fallback@^1.6.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
-connect@^3.4.0:
- version "3.7.0"
- resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
- integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
- dependencies:
- debug "2.6.9"
- finalhandler "1.1.2"
- parseurl "~1.3.3"
- utils-merge "1.0.1"
-
console-browserify@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
@@ -12041,11 +12019,6 @@ dashdash@^1.12.0:
dependencies:
assert-plus "^1.0.0"
-dashify@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.1.0.tgz#107daf9cca5e326e30a8b39ffa5048b6684922ea"
- integrity sha1-EH2vnMpeMm4wqLOf+lBItmhJIuo=
-
data-uri-to-buffer@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz#18ae979a6a0ca994b0625853916d2662bbae0b1a"
@@ -14434,7 +14407,7 @@ fbjs@^0.8.1, fbjs@^0.8.9:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
-fd-slicer@1.1.0, fd-slicer@~1.1.0:
+fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
@@ -14586,7 +14559,7 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
-finalhandler@1.1.2, finalhandler@~1.1.2:
+finalhandler@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@@ -15384,7 +15357,7 @@ glob-to-regexp@^0.4.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
-glob-watcher@5.0.3, glob-watcher@^5.0.3:
+glob-watcher@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.3.tgz#88a8abf1c4d131eb93928994bc4a593c2e5dd626"
integrity sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==
@@ -16408,7 +16381,7 @@ http-deceiver@^1.2.7:
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
-http-errors@1.7.2, http-errors@~1.7.0, http-errors@~1.7.2:
+http-errors@1.7.2, http-errors@~1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
@@ -16579,11 +16552,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
-idx@^2.5.6:
- version "2.5.6"
- resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.6.tgz#1f824595070100ae9ad585c86db08dc74f83a59d"
- integrity sha512-WFXLF7JgPytbMgelpRY46nHz5tyDcedJ76pLV+RJWdb8h33bxFq4bdZau38DhNSzk5eVniBf1K3jwfK+Lb5nYA==
-
ieee754@^1.1.12, ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -16780,13 +16748,6 @@ inline-style-prefixer@^4.0.0:
bowser "^1.7.3"
css-in-js-utils "^2.0.0"
-inline-style@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b"
- integrity sha1-L6nPYkWWqBCTVbklCU4Ti71eops=
- dependencies:
- dashify "^0.1.0"
-
inquirer@^7.0.0, inquirer@^7.3.3:
version "7.3.3"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
@@ -19234,7 +19195,7 @@ loader-utils@2.0.0, loader-utils@^2.0.0:
emojis-list "^3.0.0"
json5 "^2.1.2"
-loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.0.4, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
+loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
@@ -20266,7 +20227,7 @@ minimist-options@4.1.0:
is-plain-obj "^1.1.0"
kind-of "^6.0.3"
-minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0:
+minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.0:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
@@ -20368,13 +20329,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-mkdirp@0.5.1:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
- integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
- dependencies:
- minimist "0.0.8"
-
mkdirp@^0.3.5:
version "0.3.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7"
@@ -20481,16 +20435,6 @@ mock-fs@^5.1.2:
resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7"
integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A==
-mock-http-server@1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/mock-http-server/-/mock-http-server-1.3.0.tgz#d2c2ffe65f77d3a4da8302c91d3bf687e5b51519"
- integrity sha512-WC1fQ4kfOiiRZZ6IEOispJcfvz66m7VVbVFmnWsv1pOwL3psqYyLQGjFXg//zjPeZ//y/rxa8e2eh1Bb58cN7g==
- dependencies:
- body-parser "^1.18.1"
- connect "^3.4.0"
- multiparty "^4.1.2"
- underscore "^1.8.3"
-
module-deps@^6.2.3:
version "6.2.3"
resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee"
@@ -20642,16 +20586,6 @@ multimatch@^4.0.0:
arrify "^2.0.1"
minimatch "^3.0.4"
-multiparty@^4.1.2:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/multiparty/-/multiparty-4.2.1.tgz#d9b6c46d8b8deab1ee70c734b0af771dd46e0b13"
- integrity sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA==
- dependencies:
- fd-slicer "1.1.0"
- http-errors "~1.7.0"
- safe-buffer "5.1.2"
- uid-safe "2.1.5"
-
murmurhash-js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51"
@@ -22050,13 +21984,6 @@ parse-json@^5.0.0:
json-parse-better-errors "^1.0.1"
lines-and-columns "^1.1.6"
-parse-link-header@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7"
- integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc=
- dependencies:
- xtend "~4.0.1"
-
parse-ms@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
@@ -22232,7 +22159,7 @@ pathval@^1.1.0:
resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d"
integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==
-pbf@3.2.1, pbf@^3.0.5, pbf@^3.2.1:
+pbf@3.2.1, pbf@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a"
integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==
@@ -22379,13 +22306,6 @@ pixelmatch@^4.0.2:
dependencies:
pngjs "^3.0.0"
-pixelmatch@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.1.0.tgz#b640f0e5a03a09f235a4b818ef3b9b98d9d0b911"
- integrity sha512-HqtgvuWN12tBzKJf7jYsc38Ha28Q2NYpmBL9WostEGgDHJqbTLkjydZXL1ZHM02ZnB+Dkwlxo87HBY38kMiD6A==
- dependencies:
- pngjs "^3.4.0"
-
pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -23603,11 +23523,6 @@ randexp@0.4.6:
discontinuous-range "1.0.0"
ret "~0.1.10"
-random-bytes@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b"
- integrity sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=
-
random-word-slugs@^0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/random-word-slugs/-/random-word-slugs-0.0.5.tgz#6ccd6c7ea320be9fbc19507f8c3a7d4a970ff61f"
@@ -25618,16 +25533,6 @@ sass-loader@^10.2.0:
schema-utils "^3.0.0"
semver "^7.3.2"
-sass-resources-loader@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.0.1.tgz#c8427f3760bf7992f24f27d3889a1c797e971d3a"
- integrity sha512-UsjQWm01xglINC1kPidYwKOBBzOElVupm9RwtOkRlY0hPA4GKi2KFsn4BZypRD1kudaXgUnGnfbiVOE7c+ybAg==
- dependencies:
- async "^2.1.4"
- chalk "^1.1.3"
- glob "^7.1.1"
- loader-utils "^1.0.4"
-
save-pixels@^2.3.2:
version "2.3.4"
resolved "https://registry.yarnpkg.com/save-pixels/-/save-pixels-2.3.4.tgz#49d349c06b8d7c0127dbf0da24b44aca5afb59fe"
@@ -27478,11 +27383,6 @@ syntax-error@^1.1.1:
dependencies:
acorn-node "^1.2.0"
-tabbable@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-1.1.3.tgz#0e4ee376f3631e42d7977a074dbd2b3827843081"
- integrity sha512-nOWwx35/JuDI4ONuF0ZTo6lYvI0fY0tZCH1ErzY2EXfu4az50ZyiUX8X073FLiZtmWUVlkRnuXsehjJgCw9tYg==
-
tabbable@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.2.1.tgz#e3fda7367ddbb172dcda9f871c0fdb36d1c4cd9c"
@@ -27567,16 +27467,6 @@ tar-fs@^2.0.0, tar-fs@^2.1.1:
pump "^3.0.0"
tar-stream "^2.1.4"
-tar-fs@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.0.tgz#d1cdd121ab465ee0eb9ccde2d35049d3f3daf0d5"
- integrity sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==
- dependencies:
- chownr "^1.1.1"
- mkdirp-classic "^0.5.2"
- pump "^3.0.0"
- tar-stream "^2.0.0"
-
tar-stream@^2.0.0:
version "2.1.3"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.3.tgz#1e2022559221b7866161660f118255e20fa79e41"
@@ -28447,13 +28337,6 @@ uglify-to-browserify@~1.0.0:
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc=
-uid-safe@2.1.5:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
- integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
- dependencies:
- random-bytes "~1.0.0"
-
umd@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
@@ -28506,7 +28389,7 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
-underscore@^1.13.1, underscore@^1.8.3:
+underscore@^1.13.1:
version "1.13.1"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
@@ -29726,15 +29609,6 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
-vt-pbf@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.1.tgz#b0f627e39a10ce91d943b898ed2363d21899fb82"
- integrity sha512-pHjWdrIoxurpmTcbfBWXaPwSmtPAHS105253P1qyEfSTV2HJddqjM+kIHquaT/L6lVJIk9ltTGc0IxR/G47hYA==
- dependencies:
- "@mapbox/point-geometry" "0.1.0"
- "@mapbox/vector-tile" "^1.3.1"
- pbf "^3.0.5"
-
vt-pbf@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/vt-pbf/-/vt-pbf-3.1.3.tgz#68fd150756465e2edae1cc5c048e063916dcfaac"